diff --git a/packages/anoncreds-rs/src/services/__tests__/AnonCredsRsServices.test.ts b/packages/anoncreds-rs/src/services/__tests__/AnonCredsRsServices.test.ts index 019063bcbb..e0ba7089a9 100644 --- a/packages/anoncreds-rs/src/services/__tests__/AnonCredsRsServices.test.ts +++ b/packages/anoncreds-rs/src/services/__tests__/AnonCredsRsServices.test.ts @@ -48,7 +48,7 @@ const agentContext = getAgentContext({ describe('AnonCredsRsServices', () => { test('issuance flow without revocation', async () => { - const issuerId = 'issuer:uri' + const issuerId = 'did:indy:pool:localtest:TL1EaPFCZ8Si5aUrqScBDt' const schema = await anonCredsIssuerService.createSchema(agentContext, { attrNames: ['name', 'age'], diff --git a/packages/anoncreds-rs/tests/indy-flow.test.ts b/packages/anoncreds-rs/tests/indy-flow.test.ts index d3b53ed6fe..eadaffd740 100644 --- a/packages/anoncreds-rs/tests/indy-flow.test.ts +++ b/packages/anoncreds-rs/tests/indy-flow.test.ts @@ -69,7 +69,7 @@ const legacyIndyCredentialFormatService = new LegacyIndyCredentialFormatService( const legacyIndyProofFormatService = new LegacyIndyProofFormatService() // This is just so we don't have to register an actually indy did (as we don't have the indy did registrar configured) -const indyDid = 'TL1EaPFCZ8Si5aUrqScBDt' +const indyDid = 'did:indy:pool:localtest:TL1EaPFCZ8Si5aUrqScBDt' describe('Legacy indy format services using anoncreds-rs', () => { test('issuance and verification flow starting from proposal without negotiation and without revocation', async () => { 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 2a8e628097..4606899650 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 @@ -22,7 +22,13 @@ import { IndySdkWallet, } from '../../../../indy-sdk/src' import { IndySdkRevocationService } from '../../../../indy-sdk/src/anoncreds/services/IndySdkRevocationService' -import { indyDidFromPublicKeyBase58 } from '../../../../indy-sdk/src/utils/did' +import { + getLegacyCredentialDefinitionId, + getLegacySchemaId, + parseCredentialDefinitionId, + parseSchemaId, +} from '../../../../indy-sdk/src/anoncreds/utils/identifiers' +import { legacyIndyDidFromPublicKeyBase58 } from '../../../../indy-sdk/src/utils/did' import { InMemoryAnonCredsRegistry } from '../../../tests/InMemoryAnonCredsRegistry' import { AnonCredsModuleConfig } from '../../AnonCredsModuleConfig' import { AnonCredsLinkSecretRecord, AnonCredsLinkSecretRepository } from '../../repository' @@ -79,7 +85,8 @@ describe('Legacy indy format services', () => { test('issuance and verification flow starting from proposal without negotiation and without revocation', async () => { // This is just so we don't have to register an actual indy did (as we don't have the indy did registrar configured) const key = await wallet.createKey({ keyType: KeyType.Ed25519 }) - const indyDid = indyDidFromPublicKeyBase58(key.publicKeyBase58) + const unqualifiedIndyDid = legacyIndyDidFromPublicKeyBase58(key.publicKeyBase58) + const indyDid = `did:indy:pool1:${unqualifiedIndyDid}` // Create link secret await anonCredsHolderService.createLinkSecret(agentContext, { @@ -155,6 +162,12 @@ describe('Legacy indy format services', () => { }), ] + const cd = parseCredentialDefinitionId(credentialDefinitionState.credentialDefinitionId) + const legacyCredentialDefinitionId = getLegacyCredentialDefinitionId(cd.didIdentifier, cd.schemaSeqNo, cd.tag) + + const s = parseSchemaId(schemaState.schemaId) + const legacySchemaId = getLegacySchemaId(s.didIdentifier, s.schemaName, s.schemaVersion) + // Holder creates proposal holderCredentialRecord.credentialAttributes = credentialAttributes const { attachment: proposalAttachment } = await indyCredentialFormatService.createProposal(agentContext, { @@ -162,7 +175,7 @@ describe('Legacy indy format services', () => { credentialFormats: { indy: { attributes: credentialAttributes, - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + credentialDefinitionId: legacyCredentialDefinitionId, }, }, }) @@ -225,16 +238,16 @@ describe('Legacy indy format services', () => { age: '25', name: 'John', }, - schemaId: schemaState.schemaId, - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + schemaId: legacySchemaId, + credentialDefinitionId: legacyCredentialDefinitionId, revocationRegistryId: null, credentialRevocationId: null, }) expect(holderCredentialRecord.metadata.data).toEqual({ '_anonCreds/anonCredsCredential': { - schemaId: schemaState.schemaId, - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + schemaId: legacySchemaId, + credentialDefinitionId: legacyCredentialDefinitionId, }, '_anonCreds/anonCredsCredentialRequest': { master_secret_blinding_data: expect.any(Object), @@ -245,8 +258,8 @@ describe('Legacy indy format services', () => { expect(issuerCredentialRecord.metadata.data).toEqual({ '_anonCreds/anonCredsCredential': { - schemaId: schemaState.schemaId, - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + schemaId: legacySchemaId, + credentialDefinitionId: legacyCredentialDefinitionId, }, }) @@ -267,14 +280,14 @@ describe('Legacy indy format services', () => { attributes: [ { name: 'name', - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + credentialDefinitionId: legacyCredentialDefinitionId, value: 'John', referent: '1', }, ], predicates: [ { - credentialDefinitionId: credentialDefinitionState.credentialDefinitionId, + credentialDefinitionId: legacyCredentialDefinitionId, name: 'age', predicate: '>=', threshold: 18, diff --git a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts index 18bd9cfaab..abd3ddd6ed 100644 --- a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts +++ b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts @@ -19,6 +19,15 @@ import type { AgentContext } from '@aries-framework/core' import { Hasher, TypedArrayEncoder } from '@aries-framework/core' import BigNumber from 'bn.js' +import { + getDidIndyCredentialDefinitionId, + getDidIndySchemaId, + getLegacyCredentialDefinitionId, + getLegacySchemaId, + parseSchemaId, +} from '../../indy-sdk/src/anoncreds/utils/identifiers' +import { parseIndyDid } from '../../indy-sdk/src/dids/didIndyUtil' + /** * In memory implementation of the {@link AnonCredsRegistry} interface. Useful for testing. */ @@ -26,7 +35,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { // Roughly match that the identifier starts with an unqualified indy did. Once the // anoncreds tests are not based on the indy-sdk anymore, we can use any identifier // we want, but the indy-sdk is picky about the identifier format. - public readonly supportedIdentifier = /^[a-zA-Z0-9]{21,22}/ + public readonly supportedIdentifier = /.+/ private schemas: Record private credentialDefinitions: Record @@ -52,7 +61,11 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { public async getSchema(agentContext: AgentContext, schemaId: string): Promise { const schema = this.schemas[schemaId] - const indyLedgerSeqNo = getSeqNoFromSchemaId(schemaId) + + const parsed = parseSchemaId(schemaId) + + const legacySchemaId = getLegacySchemaId(parsed.didIdentifier, parsed.schemaName, parsed.schemaVersion) + const indyLedgerSeqNo = getSeqNoFromSchemaId(legacySchemaId) if (!schema) { return { @@ -81,10 +94,17 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { agentContext: AgentContext, options: RegisterSchemaOptions ): Promise { - const schemaId = `${options.schema.issuerId}:2:${options.schema.name}:${options.schema.version}` - const indyLedgerSeqNo = getSeqNoFromSchemaId(schemaId) + const { id: didIdentifier, namespace } = parseIndyDid(options.schema.issuerId) + const didIndySchemaId = getDidIndySchemaId(namespace, didIdentifier, options.schema.name, options.schema.version) + const legacySchemaId = getLegacySchemaId(didIdentifier, options.schema.name, options.schema.version) - this.schemas[schemaId] = options.schema + const indyLedgerSeqNo = getSeqNoFromSchemaId(legacySchemaId) + + this.schemas[didIndySchemaId] = options.schema + this.schemas[legacySchemaId] = { + ...options.schema, + issuerId: didIdentifier, + } return { registrationMetadata: {}, @@ -96,7 +116,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { schemaState: { state: 'finished', schema: options.schema, - schemaId, + schemaId: didIndySchemaId, }, } } @@ -130,10 +150,34 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { agentContext: AgentContext, options: RegisterCredentialDefinitionOptions ): Promise { - const indyLedgerSeqNo = getSeqNoFromSchemaId(options.credentialDefinition.schemaId) - const credentialDefinitionId = `${options.credentialDefinition.issuerId}:3:CL:${indyLedgerSeqNo}:${options.credentialDefinition.tag}` - - this.credentialDefinitions[credentialDefinitionId] = options.credentialDefinition + const parsedSchema = parseSchemaId(options.credentialDefinition.schemaId) + const legacySchemaId = getLegacySchemaId( + parsedSchema.didIdentifier, + parsedSchema.schemaName, + parsedSchema.schemaVersion + ) + const indyLedgerSeqNo = getSeqNoFromSchemaId(legacySchemaId) + + const { id: didIdentifier, namespace } = parseIndyDid(options.credentialDefinition.issuerId) + + const didIndyCredentialDefinitionId = getDidIndyCredentialDefinitionId( + namespace, + didIdentifier, + indyLedgerSeqNo, + options.credentialDefinition.tag + ) + const legacyCredentialDefinitionId = getLegacyCredentialDefinitionId( + didIdentifier, + indyLedgerSeqNo, + options.credentialDefinition.tag + ) + + this.credentialDefinitions[didIndyCredentialDefinitionId] = options.credentialDefinition + this.credentialDefinitions[legacyCredentialDefinitionId] = { + ...options.credentialDefinition, + issuerId: didIdentifier, + schemaId: legacySchemaId, + } return { registrationMetadata: {}, @@ -141,7 +185,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { credentialDefinitionState: { state: 'finished', credentialDefinition: options.credentialDefinition, - credentialDefinitionId, + credentialDefinitionId: didIndyCredentialDefinitionId, }, } } diff --git a/packages/anoncreds/tests/anoncreds.test.ts b/packages/anoncreds/tests/anoncreds.test.ts index 127bbee586..1f4c32f973 100644 --- a/packages/anoncreds/tests/anoncreds.test.ts +++ b/packages/anoncreds/tests/anoncreds.test.ts @@ -123,7 +123,7 @@ describe('AnonCreds API', () => { options: {}, schema: { attrNames: ['name', 'age'], - issuerId: '6xDN7v3AiGgusRp4bqZACZ', + issuerId: 'did:indy:pool:localtest:6xDN7v3AiGgusRp4bqZACZ', name: 'Employee Credential', version: '1.0.0', }, @@ -136,11 +136,11 @@ describe('AnonCreds API', () => { state: 'finished', schema: { attrNames: ['name', 'age'], - issuerId: '6xDN7v3AiGgusRp4bqZACZ', + issuerId: 'did:indy:pool:localtest:6xDN7v3AiGgusRp4bqZACZ', name: 'Employee Credential', version: '1.0.0', }, - schemaId: '6xDN7v3AiGgusRp4bqZACZ:2:Employee Credential:1.0.0', + schemaId: 'did:indy:pool:localtest:6xDN7v3AiGgusRp4bqZACZ/anoncreds/v0/SCHEMA/Employee Credential/1.0.0', }, }) @@ -148,22 +148,22 @@ describe('AnonCreds API', () => { const anonCredsSchemaRepository = agent.dependencyManager.resolve(AnonCredsSchemaRepository) const schemaRecord = await anonCredsSchemaRepository.getBySchemaId( agent.context, - '6xDN7v3AiGgusRp4bqZACZ:2:Employee Credential:1.0.0' + 'did:indy:pool:localtest:6xDN7v3AiGgusRp4bqZACZ/anoncreds/v0/SCHEMA/Employee Credential/1.0.0' ) expect(schemaRecord).toMatchObject({ - schemaId: '6xDN7v3AiGgusRp4bqZACZ:2:Employee Credential:1.0.0', + schemaId: 'did:indy:pool:localtest:6xDN7v3AiGgusRp4bqZACZ/anoncreds/v0/SCHEMA/Employee Credential/1.0.0', schema: { attrNames: ['name', 'age'], - issuerId: '6xDN7v3AiGgusRp4bqZACZ', + issuerId: 'did:indy:pool:localtest:6xDN7v3AiGgusRp4bqZACZ', name: 'Employee Credential', version: '1.0.0', }, }) expect(schemaRecord.getTags()).toEqual({ - schemaId: '6xDN7v3AiGgusRp4bqZACZ:2:Employee Credential:1.0.0', - issuerId: '6xDN7v3AiGgusRp4bqZACZ', + schemaId: 'did:indy:pool:localtest:6xDN7v3AiGgusRp4bqZACZ/anoncreds/v0/SCHEMA/Employee Credential/1.0.0', + issuerId: 'did:indy:pool:localtest:6xDN7v3AiGgusRp4bqZACZ', schemaName: 'Employee Credential', schemaVersion: '1.0.0', }) @@ -192,7 +192,7 @@ describe('AnonCreds API', () => { keyType: KeyType.Ed25519, }) - const issuerId = 'VsKV7grR1BUE29mG2Fm2kX' + const issuerId = 'did:indy:pool:localhost:VsKV7grR1BUE29mG2Fm2kX' const credentialDefinitionResult = await agent.modules.anoncreds.registerCredentialDefinition({ credentialDefinition: { @@ -209,7 +209,7 @@ describe('AnonCreds API', () => { credentialDefinitionState: { state: 'finished', credentialDefinition: { - issuerId: 'VsKV7grR1BUE29mG2Fm2kX', + issuerId: 'did:indy:pool:localhost:VsKV7grR1BUE29mG2Fm2kX', tag: 'TAG', schemaId: '7Cd2Yj9yEZNcmNoH54tq9i:2:Test Schema:1.0.0', type: 'CL', @@ -227,7 +227,7 @@ describe('AnonCreds API', () => { }, }, }, - credentialDefinitionId: 'VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG', + credentialDefinitionId: 'did:indy:pool:localhost:VsKV7grR1BUE29mG2Fm2kX/anoncreds/v0/CLAIM_DEF/75206/TAG', }, }) @@ -237,13 +237,13 @@ describe('AnonCreds API', () => { ) const credentialDefinitionRecord = await anonCredsCredentialDefinitionRepository.getByCredentialDefinitionId( agent.context, - 'VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG' + 'did:indy:pool:localhost:VsKV7grR1BUE29mG2Fm2kX/anoncreds/v0/CLAIM_DEF/75206/TAG' ) expect(credentialDefinitionRecord).toMatchObject({ - credentialDefinitionId: 'VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG', + credentialDefinitionId: 'did:indy:pool:localhost:VsKV7grR1BUE29mG2Fm2kX/anoncreds/v0/CLAIM_DEF/75206/TAG', credentialDefinition: { - issuerId: 'VsKV7grR1BUE29mG2Fm2kX', + issuerId: 'did:indy:pool:localhost:VsKV7grR1BUE29mG2Fm2kX', tag: 'TAG', schemaId: '7Cd2Yj9yEZNcmNoH54tq9i:2:Test Schema:1.0.0', type: 'CL', @@ -264,9 +264,9 @@ describe('AnonCreds API', () => { }) expect(credentialDefinitionRecord.getTags()).toEqual({ - credentialDefinitionId: 'VsKV7grR1BUE29mG2Fm2kX:3:CL:75206:TAG', + credentialDefinitionId: 'did:indy:pool:localhost:VsKV7grR1BUE29mG2Fm2kX/anoncreds/v0/CLAIM_DEF/75206/TAG', schemaId: '7Cd2Yj9yEZNcmNoH54tq9i:2:Test Schema:1.0.0', - issuerId: 'VsKV7grR1BUE29mG2Fm2kX', + issuerId: 'did:indy:pool:localhost:VsKV7grR1BUE29mG2Fm2kX', tag: 'TAG', }) }) diff --git a/packages/anoncreds/tests/legacyAnonCredsSetup.ts b/packages/anoncreds/tests/legacyAnonCredsSetup.ts index 32a876fd1a..5895e5c075 100644 --- a/packages/anoncreds/tests/legacyAnonCredsSetup.ts +++ b/packages/anoncreds/tests/legacyAnonCredsSetup.ts @@ -48,10 +48,17 @@ import { import testLogger from '../../core/tests/logger' import { IndySdkAnonCredsRegistry, + IndySdkIndyDidRegistrar, + IndySdkIndyDidResolver, IndySdkModule, - IndySdkSovDidRegistrar, IndySdkSovDidResolver, } from '../../indy-sdk/src' +import { + getLegacyCredentialDefinitionId, + getLegacySchemaId, + parseCredentialDefinitionId, + parseSchemaId, +} from '../../indy-sdk/src/anoncreds/utils/identifiers' import { getIndySdkModuleConfig } from '../../indy-sdk/tests/setupIndySdkModule' import { IndyVdrAnonCredsRegistry, IndyVdrSovDidResolver, IndyVdrModule } from '../../indy-vdr/src' import { @@ -98,8 +105,8 @@ export const getLegacyAnonCredsModules = ({ registries: [new IndySdkAnonCredsRegistry()], }), dids: new DidsModule({ - resolvers: [new IndySdkSovDidResolver()], - registrars: [new IndySdkSovDidRegistrar()], + resolvers: [new IndySdkSovDidResolver(), new IndySdkIndyDidResolver()], + registrars: [new IndySdkIndyDidRegistrar()], }), indySdk: new IndySdkModule(getIndySdkModuleConfig()), cache: new CacheModule({ @@ -444,14 +451,15 @@ export async function setupAnonCredsTests< export async function prepareForAnonCredsIssuance(agent: Agent, { attributeNames }: { attributeNames: string[] }) { // Add existing endorser did to the wallet - const issuerId = await importExistingIndyDidFromPrivateKey(agent, TypedArrayEncoder.fromString(publicDidSeed)) + const unqualifiedDid = await importExistingIndyDidFromPrivateKey(agent, TypedArrayEncoder.fromString(publicDidSeed)) + const didIndyDid = `did:indy:pool:localtest:${unqualifiedDid}` const schema = await registerSchema(agent, { // TODO: update attrNames to attributeNames attrNames: attributeNames, name: `Schema ${randomUUID()}`, version: '1.0', - issuerId, + issuerId: didIndyDid, }) // Wait some time pass to let ledger settle the object @@ -459,16 +467,31 @@ export async function prepareForAnonCredsIssuance(agent: Agent, { attributeNames const credentialDefinition = await registerCredentialDefinition(agent, { schemaId: schema.schemaId, - issuerId, + issuerId: didIndyDid, tag: 'default', }) + const s = parseSchemaId(schema.schemaId) + const cd = parseCredentialDefinitionId(credentialDefinition.credentialDefinitionId) + + const legacySchemaId = getLegacySchemaId(s.didIdentifier, s.schemaName, s.schemaVersion) + const legacyCredentialDefinitionId = getLegacyCredentialDefinitionId(cd.didIdentifier, cd.schemaSeqNo, cd.tag) + // Wait some time pass to let ledger settle the object await sleep(1000) + // NOTE: we return the legacy schema and credential definition ids here because that's what currently expected + // in all tests. If we also support did:indy in tests we probably want to return the qualified identifiers here + // and transform them to the legacy variant in the specific tests that need it. return { - schema, - credentialDefinition, + schema: { + ...schema, + schemaId: legacySchemaId, + }, + credentialDefinition: { + ...credentialDefinition, + credentialDefinitionId: legacyCredentialDefinitionId, + }, } } @@ -478,9 +501,7 @@ async function registerSchema( ): Promise { const { schemaState } = await agent.modules.anoncreds.registerSchema({ schema, - options: { - didIndyNamespace: 'pool:localtest', - }, + options: {}, }) testLogger.test(`created schema with id ${schemaState.schemaId}`, schema) @@ -500,9 +521,7 @@ async function registerCredentialDefinition( ): Promise { const { credentialDefinitionState } = await agent.modules.anoncreds.registerCredentialDefinition({ credentialDefinition, - options: { - didIndyNamespace: 'pool:localtest', - }, + options: {}, }) if (credentialDefinitionState.state !== 'finished') { diff --git a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials.propose-offerED25519.test.ts b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials.propose-offerED25519.test.ts index 8c0956a145..6c514127ce 100644 --- a/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials.propose-offerED25519.test.ts +++ b/packages/core/src/modules/credentials/protocol/v2/__tests__/v2.ldproof.credentials.propose-offerED25519.test.ts @@ -12,8 +12,9 @@ import { import { prepareForAnonCredsIssuance } from '../../../../../../../anoncreds/tests/legacyAnonCredsSetup' import { IndySdkAnonCredsRegistry, + IndySdkIndyDidRegistrar, + IndySdkIndyDidResolver, IndySdkModule, - IndySdkSovDidRegistrar, IndySdkSovDidResolver, } from '../../../../../../../indy-sdk/src' import { indySdk } from '../../../../../../../indy-sdk/tests/setupIndySdkModule' @@ -107,8 +108,8 @@ const getIndyJsonLdModules = () => registries: [new IndySdkAnonCredsRegistry()], }), dids: new DidsModule({ - resolvers: [new IndySdkSovDidResolver(), new KeyDidResolver()], - registrars: [new IndySdkSovDidRegistrar(), new KeyDidRegistrar()], + resolvers: [new IndySdkSovDidResolver(), new IndySdkIndyDidResolver(), new KeyDidResolver()], + registrars: [new IndySdkIndyDidRegistrar(), new KeyDidRegistrar()], }), indySdk: new IndySdkModule({ indySdk, diff --git a/packages/core/src/modules/oob/__tests__/implicit.e2e.test.ts b/packages/core/src/modules/oob/__tests__/implicit.e2e.test.ts index ae0c98e6d7..846a139a87 100644 --- a/packages/core/src/modules/oob/__tests__/implicit.e2e.test.ts +++ b/packages/core/src/modules/oob/__tests__/implicit.e2e.test.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import type { IndySdkSovDidCreateOptions } from '@aries-framework/indy-sdk' +import type { IndySdkIndyDidCreateOptions } from '@aries-framework/indy-sdk' import { getLegacyAnonCredsModules } from '../../../../../anoncreds/tests/legacyAnonCredsSetup' import { setupSubjectTransports } from '../../../../tests' @@ -198,10 +198,10 @@ describe('out of band implicit', () => { }) async function createPublicDid(agent: Agent, unqualifiedSubmitterDid: string, endpoint: string) { - const createResult = await agent.dids.create({ - method: 'sov', + const createResult = await agent.dids.create({ + method: 'indy', options: { - submitterVerificationMethod: `did:sov:${unqualifiedSubmitterDid}#key-1`, + submitterDid: `did:indy:pool:localtest:${unqualifiedSubmitterDid}`, alias: 'Alias', endpoints: { endpoint, diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts index 2dbf79cd53..fe0d30e35b 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkAnonCredsRegistry.ts @@ -1,3 +1,4 @@ +import type { IndySdkPool } from '../../ledger' import type { IndySdk } from '../../types' import type { AnonCredsRegistry, @@ -13,23 +14,22 @@ import type { import type { AgentContext } from '@aries-framework/core' import type { Schema as IndySdkSchema } from 'indy-sdk' -import { DidsApi, getKeyFromVerificationMethod } from '@aries-framework/core' - +import { parseIndyDid, verificationKeyForIndyDid } from '../../dids/didIndyUtil' import { IndySdkError, isIndyError } from '../../error' import { IndySdkPoolService } from '../../ledger' import { IndySdkSymbol } from '../../types' import { - didFromCredentialDefinitionId, - didFromRevocationRegistryDefinitionId, - didFromSchemaId, + getDidIndyCredentialDefinitionId, + getDidIndySchemaId, getLegacyCredentialDefinitionId, + getLegacyRevocationRegistryId, getLegacySchemaId, indySdkAnonCredsRegistryIdentifierRegex, + parseCredentialDefinitionId, + parseRevocationRegistryId, + parseSchemaId, } from '../utils/identifiers' -import { - anonCredsRevocationStatusListFromIndySdk, - anonCredsRevocationRegistryDefinitionFromIndySdk, -} from '../utils/transform' +import { anonCredsRevocationStatusListFromIndySdk } from '../utils/transform' /** * TODO: validation of the identifiers. The Indy SDK classes only support the legacy (unqualified) identifiers. @@ -47,11 +47,14 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) - const did = didFromSchemaId(schemaId) + // parse schema id (supports did:indy and legacy) + const { did, didIdentifier, schemaName, schemaVersion } = parseSchemaId(schemaId) const { pool } = await indySdkPoolService.getPoolForDid(agentContext, did) agentContext.config.logger.debug(`Getting schema '${schemaId}' from ledger '${pool.didIndyNamespace}'`) - const request = await indySdk.buildGetSchemaRequest(null, schemaId) + // even though we support did:indy and legacy identifiers we always need to fetch using the legacy identifier + const legacySchemaId = getLegacySchemaId(didIdentifier, schemaName, schemaVersion) + const request = await indySdk.buildGetSchemaRequest(null, legacySchemaId) agentContext.config.logger.trace( `Submitting get schema request for schema '${schemaId}' to ledger '${pool.didIndyNamespace}'` @@ -67,16 +70,14 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { schema, }) - const issuerId = didFromSchemaId(schema.id) - return { schema: { attrNames: schema.attrNames, name: schema.name, version: schema.version, - issuerId: issuerId, + issuerId: did, }, - schemaId: schema.id, + schemaId, resolutionMetadata: {}, schemaMetadata: { didIndyNamespace: pool.didIndyNamespace, @@ -104,62 +105,37 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { public async registerSchema( agentContext: AgentContext, - options: IndySdkRegisterSchemaOptions + options: RegisterSchemaOptions ): Promise { - // Make sure didIndyNamespace is passed - if (!options.options.didIndyNamespace) { - return { - schemaMetadata: {}, - registrationMetadata: {}, - schemaState: { - reason: 'no didIndyNamespace defined in the options. didIndyNamespace is required when using the Indy SDK', - schema: options.schema, - state: 'failed', - }, - } - } - try { + // This will throw an error if trying to register a schema with a legacy indy identifier. We only support did:indy identifiers + // for registering, that will allow us to extract the namespace and means all stored records will use did:indy identifiers. + const { id: unqualifiedDid, namespace } = parseIndyDid(options.schema.issuerId) + const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) - const pool = indySdkPoolService.getPoolForNamespace(options.options.didIndyNamespace) + const pool = indySdkPoolService.getPoolForNamespace(namespace) agentContext.config.logger.debug( `Register schema on ledger '${pool.didIndyNamespace}' with did '${options.schema.issuerId}'`, options.schema ) + const didIndySchemaId = getDidIndySchemaId(namespace, unqualifiedDid, options.schema.name, options.schema.version) + const legacySchemaId = getLegacySchemaId(unqualifiedDid, options.schema.name, options.schema.version) + const schema = { attrNames: options.schema.attrNames, name: options.schema.name, version: options.schema.version, - id: getLegacySchemaId(options.schema.issuerId, options.schema.name, options.schema.version), + id: legacySchemaId, ver: '1.0', // Casted as because the type expect a seqNo, but that's not actually required for the input of // buildSchemaRequest (seqNo is not yet known) } as IndySdkSchema - const request = await indySdk.buildSchemaRequest(options.schema.issuerId, schema) - - // FIXME: we should store the didDocument in the DidRecord so we don't have to fetch our own did - // from the ledger to know which key is associated with the did - const didsApi = agentContext.dependencyManager.resolve(DidsApi) - const didResult = await didsApi.resolve(`did:sov:${options.schema.issuerId}`) - - if (!didResult.didDocument) { - return { - schemaMetadata: {}, - registrationMetadata: {}, - schemaState: { - schema: options.schema, - state: 'failed', - reason: `didNotFound: unable to resolve did did:sov:${options.schema.issuerId}: ${didResult.didResolutionMetadata.message}`, - }, - } - } - - const verificationMethod = didResult.didDocument.dereferenceKey(`did:sov:${options.schema.issuerId}#key-1`) - const submitterKey = getKeyFromVerificationMethod(verificationMethod) + const request = await indySdk.buildSchemaRequest(unqualifiedDid, schema) + const submitterKey = await verificationKeyForIndyDid(agentContext, options.schema.issuerId) const response = await indySdkPoolService.submitWriteRequest(agentContext, pool, request, submitterKey) agentContext.config.logger.debug(`Registered schema '${schema.id}' on ledger '${pool.didIndyNamespace}'`, { @@ -176,14 +152,13 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { name: schema.name, version: schema.version, }, - schemaId: schema.id, + schemaId: didIndySchemaId, }, registrationMetadata: {}, schemaMetadata: { // NOTE: the seqNo is required by the indy-sdk even though not present in AnonCreds v1. // For this reason we return it in the metadata. indyLedgerSeqNo: response.result.txnMetadata.seqNo, - didIndyNamespace: pool.didIndyNamespace, }, } } catch (error) { @@ -213,13 +188,16 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) - const did = didFromCredentialDefinitionId(credentialDefinitionId) + // we support did:indy and legacy identifiers + const { did, didIdentifier, schemaSeqNo, tag } = parseCredentialDefinitionId(credentialDefinitionId) const { pool } = await indySdkPoolService.getPoolForDid(agentContext, did) agentContext.config.logger.debug( `Using ledger '${pool.didIndyNamespace}' to retrieve credential definition '${credentialDefinitionId}'` ) - const request = await indySdk.buildGetCredDefRequest(null, credentialDefinitionId) + + const legacyCredentialDefinitionId = getLegacyCredentialDefinitionId(didIdentifier, schemaSeqNo, tag) + const request = await indySdk.buildGetCredDefRequest(null, legacyCredentialDefinitionId) agentContext.config.logger.trace( `Submitting get credential definition request for credential definition '${credentialDefinitionId}' to ledger '${pool.didIndyNamespace}'` @@ -234,7 +212,7 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { ) const [, credentialDefinition] = await indySdk.parseGetCredDefResponse(response) - const { schema } = await this.fetchIndySchemaWithSeqNo(agentContext, Number(credentialDefinition.schemaId), did) + const { schema } = await this.fetchIndySchemaWithSeqNo(agentContext, pool, Number(credentialDefinition.schemaId)) if (credentialDefinition && schema) { agentContext.config.logger.debug( @@ -244,11 +222,16 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { } ) + // Format the schema id based on the type of the credential definition id + const schemaId = credentialDefinitionId.startsWith('did:indy') + ? getDidIndySchemaId(pool.didIndyNamespace, didIdentifier, schema.name, schema.version) + : schema.schemaId + return { - credentialDefinitionId: credentialDefinition.id, + credentialDefinitionId, credentialDefinition: { - issuerId: didFromCredentialDefinitionId(credentialDefinition.id), - schemaId: schema.schemaId, + issuerId: did, + schemaId, tag: credentialDefinition.tag, type: 'CL', value: credentialDefinition.value, @@ -291,31 +274,23 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { public async registerCredentialDefinition( agentContext: AgentContext, - options: IndySdkRegisterCredentialDefinitionOptions + options: RegisterCredentialDefinitionOptions ): Promise { - // Make sure didIndyNamespace is passed - if (!options.options.didIndyNamespace) { - return { - credentialDefinitionMetadata: {}, - registrationMetadata: {}, - credentialDefinitionState: { - reason: 'no didIndyNamespace defined in the options. didIndyNamespace is required when using the Indy SDK', - credentialDefinition: options.credentialDefinition, - state: 'failed', - }, - } - } - try { + // This will throw an error if trying to register a credential defintion with a legacy indy identifier. We only support did:indy + // identifiers for registering, that will allow us to extract the namespace and means all stored records will use did:indy identifiers. + const { id: unqualifiedDid, namespace } = parseIndyDid(options.credentialDefinition.issuerId) + const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) - const pool = indySdkPoolService.getPoolForNamespace(options.options.didIndyNamespace) + const pool = indySdkPoolService.getPoolForNamespace(namespace) agentContext.config.logger.debug( `Registering credential definition on ledger '${pool.didIndyNamespace}' with did '${options.credentialDefinition.issuerId}'`, options.credentialDefinition ) + // TODO: check structure of the schemaId // TODO: this will bypass caching if done on a higher level. const { schema, schemaMetadata, resolutionMetadata } = await this.getSchema( agentContext, @@ -336,14 +311,20 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { } } - const credentialDefinitionId = getLegacyCredentialDefinitionId( - options.credentialDefinition.issuerId, + const legacyCredentialDefinitionId = getLegacyCredentialDefinitionId( + unqualifiedDid, + schemaMetadata.indyLedgerSeqNo, + options.credentialDefinition.tag + ) + const didIndyCredentialDefinitionId = getDidIndyCredentialDefinitionId( + namespace, + unqualifiedDid, schemaMetadata.indyLedgerSeqNo, options.credentialDefinition.tag ) - const request = await indySdk.buildCredDefRequest(options.credentialDefinition.issuerId, { - id: credentialDefinitionId, + const request = await indySdk.buildCredDefRequest(unqualifiedDid, { + id: legacyCredentialDefinitionId, // Indy ledger requires the credential schemaId to be a string of the schema seqNo. schemaId: schemaMetadata.indyLedgerSeqNo.toString(), tag: options.credentialDefinition.tag, @@ -352,32 +333,12 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { ver: '1.0', }) - // FIXME: we should store the didDocument in the DidRecord so we don't have to fetch our own did - // from the ledger to know which key is associated with the did - const didsApi = agentContext.dependencyManager.resolve(DidsApi) - const didResult = await didsApi.resolve(`did:sov:${options.credentialDefinition.issuerId}`) - - if (!didResult.didDocument) { - return { - credentialDefinitionMetadata: {}, - registrationMetadata: {}, - credentialDefinitionState: { - credentialDefinition: options.credentialDefinition, - state: 'failed', - reason: `didNotFound: unable to resolve did did:sov:${options.credentialDefinition.issuerId}: ${didResult.didResolutionMetadata.message}`, - }, - } - } - - const verificationMethod = didResult.didDocument.dereferenceKey( - `did:sov:${options.credentialDefinition.issuerId}#key-1` - ) - const submitterKey = getKeyFromVerificationMethod(verificationMethod) + const submitterKey = await verificationKeyForIndyDid(agentContext, options.credentialDefinition.issuerId) const response = await indySdkPoolService.submitWriteRequest(agentContext, pool, request, submitterKey) agentContext.config.logger.debug( - `Registered credential definition '${credentialDefinitionId}' on ledger '${pool.didIndyNamespace}'`, + `Registered credential definition '${didIndyCredentialDefinitionId}' on ledger '${pool.didIndyNamespace}'`, { response, credentialDefinition: options.credentialDefinition, @@ -385,12 +346,10 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { ) return { - credentialDefinitionMetadata: { - didIndyNamespace: pool.didIndyNamespace, - }, + credentialDefinitionMetadata: {}, credentialDefinitionState: { credentialDefinition: options.credentialDefinition, - credentialDefinitionId, + credentialDefinitionId: didIndyCredentialDefinitionId, state: 'finished', }, registrationMetadata: {}, @@ -417,13 +376,21 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) - const did = didFromRevocationRegistryDefinitionId(revocationRegistryDefinitionId) + const { did, didIdentifier, credentialDefinitionTag, revocationRegistryTag, schemaSeqNo } = + parseRevocationRegistryId(revocationRegistryDefinitionId) const { pool } = await indySdkPoolService.getPoolForDid(agentContext, did) agentContext.config.logger.debug( `Using ledger '${pool.didIndyNamespace}' to retrieve revocation registry definition '${revocationRegistryDefinitionId}'` ) - const request = await indySdk.buildGetRevocRegDefRequest(null, revocationRegistryDefinitionId) + + const legacyRevocationRegistryId = getLegacyRevocationRegistryId( + didIdentifier, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryTag + ) + const request = await indySdk.buildGetRevocRegDefRequest(null, legacyRevocationRegistryId) agentContext.config.logger.trace( `Submitting get revocation registry definition request for revocation registry definition '${revocationRegistryDefinitionId}' to ledger` @@ -445,9 +412,24 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { } ) + const credentialDefinitionId = revocationRegistryDefinitionId.startsWith('did:indy:') + ? getDidIndyCredentialDefinitionId(pool.didIndyNamespace, didIdentifier, schemaSeqNo, credentialDefinitionTag) + : getLegacyCredentialDefinitionId(didIdentifier, schemaSeqNo, credentialDefinitionTag) + return { resolutionMetadata: {}, - revocationRegistryDefinition: anonCredsRevocationRegistryDefinitionFromIndySdk(revocationRegistryDefinition), + revocationRegistryDefinition: { + issuerId: did, + credDefId: credentialDefinitionId, + value: { + maxCredNum: revocationRegistryDefinition.value.maxCredNum, + publicKeys: revocationRegistryDefinition.value.publicKeys, + tailsHash: revocationRegistryDefinition.value.tailsHash, + tailsLocation: revocationRegistryDefinition.value.tailsLocation, + }, + tag: revocationRegistryDefinition.tag, + revocDefType: 'CL_ACCUM', + }, revocationRegistryDefinitionId, revocationRegistryDefinitionMetadata: { issuanceType: revocationRegistryDefinition.value.issuanceType, @@ -483,15 +465,23 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) - const did = didFromRevocationRegistryDefinitionId(revocationRegistryId) + const { did, didIdentifier, schemaSeqNo, credentialDefinitionTag, revocationRegistryTag } = + parseRevocationRegistryId(revocationRegistryId) const { pool } = await indySdkPoolService.getPoolForDid(agentContext, did) agentContext.config.logger.debug( `Using ledger '${pool.didIndyNamespace}' to retrieve revocation registry deltas with revocation registry definition id '${revocationRegistryId}' until ${timestamp}` ) + const legacyRevocationRegistryId = getLegacyRevocationRegistryId( + didIdentifier, + schemaSeqNo, + credentialDefinitionTag, + revocationRegistryTag + ) + // TODO: implement caching for returned deltas - const request = await indySdk.buildGetRevocRegDeltaRequest(null, revocationRegistryId, 0, timestamp) + const request = await indySdk.buildGetRevocRegDeltaRequest(null, legacyRevocationRegistryId, 0, timestamp) agentContext.config.logger.trace( `Submitting get revocation registry delta request for revocation registry '${revocationRegistryId}' to ledger` @@ -567,14 +557,13 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { } } - private async fetchIndySchemaWithSeqNo(agentContext: AgentContext, seqNo: number, did: string) { + private async fetchIndySchemaWithSeqNo(agentContext: AgentContext, pool: IndySdkPool, seqNo: number) { const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) - const { pool } = await indySdkPoolService.getPoolForDid(agentContext, did) agentContext.config.logger.debug(`Getting transaction with seqNo '${seqNo}' from ledger '${pool.didIndyNamespace}'`) - const request = await indySdk.buildGetTxnRequest(did, 'DOMAIN', seqNo) + const request = await indySdk.buildGetTxnRequest(null, 'DOMAIN', seqNo) agentContext.config.logger.trace(`Submitting get transaction request to ledger '${pool.didIndyNamespace}'`) const response = await indySdkPoolService.submitReadRequest(pool, request) @@ -586,15 +575,14 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { return {} } - const schemaId = getLegacySchemaId(did, schema.txn.data.data.name, schema.txn.data.data.version) - return { schema: { - schemaId, + // txnId is the schema id + schemaId: schema.txnMetadata.txnId, attr_name: schema.txn.data.data.attr_names, name: schema.txn.data.data.name, version: schema.txn.data.data.version, - issuerId: did, + issuerId: schema.txn.metadata.from, seqNo, }, indyNamespace: pool.didIndyNamespace, @@ -603,7 +591,13 @@ export class IndySdkAnonCredsRegistry implements AnonCredsRegistry { } interface SchemaType { + txnMetadata: { + txnId: string + } txn: { + metadata: { + from: string + } data: { data: { attr_names: string[] @@ -615,15 +609,3 @@ interface SchemaType { type: string } } - -export interface IndySdkRegisterSchemaOptions extends RegisterSchemaOptions { - options: { - didIndyNamespace: string - } -} - -export interface IndySdkRegisterCredentialDefinitionOptions extends RegisterCredentialDefinitionOptions { - options: { - didIndyNamespace: string - } -} diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkHolderService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkHolderService.ts index e00718c62c..d54a46a016 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkHolderService.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkHolderService.ts @@ -31,7 +31,7 @@ import { AriesFrameworkError, injectable, inject, utils } from '@aries-framework import { IndySdkError, isIndyError } from '../../error' import { IndySdk, IndySdkSymbol } from '../../types' import { assertIndySdkWallet } from '../../utils/assertIndySdkWallet' -import { getIndySeqNoFromUnqualifiedCredentialDefinitionId } from '../utils/identifiers' +import { parseCredentialDefinitionId } from '../utils/identifiers' import { indySdkCredentialDefinitionFromAnonCreds, indySdkRevocationRegistryDefinitionFromAnonCreds, @@ -105,8 +105,8 @@ export class IndySdkHolderService implements AnonCredsHolderService { ) // Get the seqNo for the schemas so we can use it when transforming the schemas - const schemaSeqNo = getIndySeqNoFromUnqualifiedCredentialDefinitionId(credentialDefinitionId) - seqNoMap[credentialDefinition.schemaId] = schemaSeqNo + const { schemaSeqNo } = parseCredentialDefinitionId(credentialDefinitionId) + seqNoMap[credentialDefinition.schemaId] = Number(schemaSeqNo) } // Convert AnonCreds schemas to Indy schemas diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts index d0d0a796d1..2b5365522b 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkIssuerService.ts @@ -15,9 +15,11 @@ import type { AgentContext } from '@aries-framework/core' import { generateLegacyProverDidLikeString } from '@aries-framework/anoncreds' import { injectable, AriesFrameworkError, inject } from '@aries-framework/core' +import { parseIndyDid } from '../../dids/didIndyUtil' import { IndySdkError, isIndyError } from '../../error' import { IndySdk, IndySdkSymbol } from '../../types' import { assertIndySdkWallet } from '../../utils/assertIndySdkWallet' +import { getLegacySchemaId } from '../utils/identifiers' import { createTailsReader } from '../utils/tails' import { indySdkSchemaFromAnonCreds } from '../utils/transform' @@ -30,11 +32,14 @@ export class IndySdkIssuerService implements AnonCredsIssuerService { } public async createSchema(agentContext: AgentContext, options: CreateSchemaOptions): Promise { - const { issuerId, name, version, attrNames } = options + // We only support passing qualified did:indy issuer ids in the indy issuer service when creating objects + const { id: unqualifiedDid } = parseIndyDid(options.issuerId) + + const { name, version, attrNames, issuerId } = options assertIndySdkWallet(agentContext.wallet) try { - const [, schema] = await this.indySdk.issuerCreateSchema(issuerId, name, version, attrNames) + const [, schema] = await this.indySdk.issuerCreateSchema(unqualifiedDid, name, version, attrNames) return { issuerId, @@ -54,6 +59,12 @@ export class IndySdkIssuerService implements AnonCredsIssuerService { ): Promise { const { tag, supportRevocation, schema, issuerId, schemaId } = options + // We only support passing qualified did:indy issuer ids in the indy issuer service when creating objects + const { id: unqualifiedDid } = parseIndyDid(options.issuerId) + + // parse schema in a way that supports both unqualified and qualified identifiers + const legacySchemaId = getLegacySchemaId(unqualifiedDid, schema.name, schema.version) + if (!metadata) throw new AriesFrameworkError('The metadata parameter is required when using Indy, but received undefined.') @@ -61,8 +72,8 @@ export class IndySdkIssuerService implements AnonCredsIssuerService { assertIndySdkWallet(agentContext.wallet) const [, credentialDefinition] = await this.indySdk.issuerCreateAndStoreCredentialDef( agentContext.wallet.handle, - issuerId, - indySdkSchemaFromAnonCreds(schemaId, schema, metadata.indyLedgerSchemaSeqNo), + unqualifiedDid, + indySdkSchemaFromAnonCreds(legacySchemaId, schema, metadata.indyLedgerSchemaSeqNo), tag, 'CL', { diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkVerifierService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkVerifierService.ts index e4e4cb1d2d..b280256229 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkVerifierService.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkVerifierService.ts @@ -6,7 +6,7 @@ import { inject, injectable } from '@aries-framework/core' import { IndySdkError, isIndyError } from '../../error' import { IndySdk, IndySdkSymbol } from '../../types' -import { getIndySeqNoFromUnqualifiedCredentialDefinitionId } from '../utils/identifiers' +import { parseCredentialDefinitionId } from '../utils/identifiers' import { indySdkCredentialDefinitionFromAnonCreds, indySdkRevocationRegistryDefinitionFromAnonCreds, @@ -39,8 +39,8 @@ export class IndySdkVerifierService implements AnonCredsVerifierService { ) // Get the seqNo for the schemas so we can use it when transforming the schemas - const schemaSeqNo = getIndySeqNoFromUnqualifiedCredentialDefinitionId(credentialDefinitionId) - seqNoMap[credentialDefinition.schemaId] = schemaSeqNo + const { schemaSeqNo } = parseCredentialDefinitionId(credentialDefinitionId) + seqNoMap[credentialDefinition.schemaId] = Number(schemaSeqNo) } // Convert AnonCreds schemas to Indy schemas diff --git a/packages/indy-sdk/src/anoncreds/utils/__tests__/identifiers.test.ts b/packages/indy-sdk/src/anoncreds/utils/__tests__/identifiers.test.ts index 76454b615a..ca1751c4e2 100644 --- a/packages/indy-sdk/src/anoncreds/utils/__tests__/identifiers.test.ts +++ b/packages/indy-sdk/src/anoncreds/utils/__tests__/identifiers.test.ts @@ -1,31 +1,55 @@ import { - didFromSchemaId, - didFromCredentialDefinitionId, - didFromRevocationRegistryDefinitionId, - getIndySeqNoFromUnqualifiedCredentialDefinitionId, + getDidIndyCredentialDefinitionId, + getDidIndyRevocationRegistryId, + getDidIndySchemaId, getLegacyCredentialDefinitionId, + getLegacyRevocationRegistryId, getLegacySchemaId, indySdkAnonCredsRegistryIdentifierRegex, + parseCredentialDefinitionId, + parseRevocationRegistryId, + parseSchemaId, } from '../identifiers' describe('identifiers', () => { - it('matches against a legacy indy did, schema id, credential definition id and revocation registry id', () => { - const did = '7Tqg6BwSSWapxgUDm9KKgg' - const schemaId = 'BQ42WeE24jFHeyGg8x9XAz:2:Medical Bill:1.0' - const credentialDefinitionId = 'N7baRMcyvPwWc8v85CtZ6e:3:CL:100669:SCH Employee ID' - const revocationRegistryId = - 'N7baRMcyvPwWc8v85CtZ6e:4:N7baRMcyvPwWc8v85CtZ6e:3:CL:100669:SCH Employee ID:CL_ACCUM:1-1024' - - const anotherId = 'some:id' - - expect(indySdkAnonCredsRegistryIdentifierRegex.test(did)).toEqual(true) - expect(indySdkAnonCredsRegistryIdentifierRegex.test(schemaId)).toEqual(true) - expect(indySdkAnonCredsRegistryIdentifierRegex.test(credentialDefinitionId)).toEqual(true) - expect(indySdkAnonCredsRegistryIdentifierRegex.test(revocationRegistryId)).toEqual(true) - expect(indySdkAnonCredsRegistryIdentifierRegex.test(anotherId)).toEqual(false) + describe('indySdkAnonCredsRegistryIdentifierRegex', () => { + test('matches against a legacy indy did, schema id, credential definition id and revocation registry id', () => { + const did = '7Tqg6BwSSWapxgUDm9KKgg' + const schemaId = 'BQ42WeE24jFHeyGg8x9XAz:2:Medical Bill:1.0' + const credentialDefinitionId = 'N7baRMcyvPwWc8v85CtZ6e:3:CL:100669:SCH Employee ID' + const revocationRegistryId = + 'N7baRMcyvPwWc8v85CtZ6e:4:N7baRMcyvPwWc8v85CtZ6e:3:CL:100669:SCH Employee ID:CL_ACCUM:1-1024' + + const anotherId = 'some:id' + + // unqualified issuerId not in regex on purpose. See note in implementation. + expect(indySdkAnonCredsRegistryIdentifierRegex.test(did)).toEqual(false) + + expect(indySdkAnonCredsRegistryIdentifierRegex.test(schemaId)).toEqual(true) + expect(indySdkAnonCredsRegistryIdentifierRegex.test(credentialDefinitionId)).toEqual(true) + expect(indySdkAnonCredsRegistryIdentifierRegex.test(revocationRegistryId)).toEqual(true) + expect(indySdkAnonCredsRegistryIdentifierRegex.test(anotherId)).toEqual(false) + }) + + test('matches against a did indy did, schema id, credential definition id and revocation registry id', () => { + const did = 'did:indy:local:7Tqg6BwSSWapxgUDm9KKgg' + const schemaId = 'did:indy:local:BQ42WeE24jFHeyGg8x9XAz/anoncreds/v0/SCHEMA/Medical Bill/1.0' + const credentialDefinitionId = + 'did:indy:local:N7baRMcyvPwWc8v85CtZ6e/anoncreds/v0/CLAIM_DEF/100669/SCH Employee ID' + const revocationRegistryId = + 'did:indy:local:N7baRMcyvPwWc8v85CtZ6e/anoncreds/v0/REV_REG_DEF/100669/SCH Employee ID/1-1024' + + const anotherId = 'did:indy:local:N7baRMcyvPwWc8v85CtZ6e/anoncreds/v0/SOME_DEF' + + expect(indySdkAnonCredsRegistryIdentifierRegex.test(did)).toEqual(true) + expect(indySdkAnonCredsRegistryIdentifierRegex.test(schemaId)).toEqual(true) + expect(indySdkAnonCredsRegistryIdentifierRegex.test(credentialDefinitionId)).toEqual(true) + expect(indySdkAnonCredsRegistryIdentifierRegex.test(revocationRegistryId)).toEqual(true) + expect(indySdkAnonCredsRegistryIdentifierRegex.test(anotherId)).toEqual(false) + }) }) - it('getLegacySchemaId should return a valid schema id given a did, name, and version', () => { + test('getLegacySchemaId returns a valid schema id given a did, name, and version', () => { const did = '12345' const name = 'backbench' const version = '420' @@ -33,7 +57,7 @@ describe('identifiers', () => { expect(getLegacySchemaId(did, name, version)).toEqual('12345:2:backbench:420') }) - it('getLegacyCredentialDefinitionId should return a valid credential definition id given a did, seqNo, and tag', () => { + test('getLegacyCredentialDefinitionId returns a valid credential definition id given a did, seqNo, and tag', () => { const did = '12345' const seqNo = 420 const tag = 'someTag' @@ -41,25 +65,121 @@ describe('identifiers', () => { expect(getLegacyCredentialDefinitionId(did, seqNo, tag)).toEqual('12345:3:CL:420:someTag') }) - it('getIndySeqNoFromUnqualifiedCredentialDefinitionId should return the seqNo from the credential definition id', () => { - expect(getIndySeqNoFromUnqualifiedCredentialDefinitionId('12345:3:CL:420:someTag')).toEqual(420) + test('getLegacyRevocationRegistryId returns a valid credential definition id given a did, seqNo, and tag', () => { + const did = '12345' + const seqNo = 420 + const credentialDefinitionTag = 'someTag' + const tag = 'anotherTag' + + expect(getLegacyRevocationRegistryId(did, seqNo, credentialDefinitionTag, tag)).toEqual( + '12345:4:12345:3:CL:420:someTag:CL_ACCUM:anotherTag' + ) + }) + + test('getDidIndySchemaId returns a valid schema id given a did, name, and version', () => { + const namespace = 'sovrin:test' + const did = '12345' + const name = 'backbench' + const version = '420' + + expect(getDidIndySchemaId(namespace, did, name, version)).toEqual( + 'did:indy:sovrin:test:12345/anoncreds/v0/SCHEMA/backbench/420' + ) + }) + + test('getDidIndyCredentialDefinitionId returns a valid credential definition id given a did, seqNo, and tag', () => { + const namespace = 'sovrin:test' + const did = '12345' + const seqNo = 420 + const tag = 'someTag' + + expect(getDidIndyCredentialDefinitionId(namespace, did, seqNo, tag)).toEqual( + 'did:indy:sovrin:test:12345/anoncreds/v0/CLAIM_DEF/420/someTag' + ) + }) + + test('getDidIndyRevocationRegistryId returns a valid credential definition id given a did, seqNo, and tag', () => { + const namespace = 'sovrin:test' + const did = '12345' + const seqNo = 420 + const credentialDefinitionTag = 'someTag' + const tag = 'anotherTag' + + expect(getDidIndyRevocationRegistryId(namespace, did, seqNo, credentialDefinitionTag, tag)).toEqual( + 'did:indy:sovrin:test:12345/anoncreds/v0/REV_REG_DEF/420/someTag/anotherTag' + ) }) - it('didFromSchemaId should return the did from the schema id', () => { - const schemaId = '12345:2:backbench:420' + describe('parseSchemaId', () => { + test('parses legacy schema id', () => { + expect(parseSchemaId('SDqTzbVuCowusqGBNbNDjH:2:schema-name:1.0')).toEqual({ + did: 'SDqTzbVuCowusqGBNbNDjH', + didIdentifier: 'SDqTzbVuCowusqGBNbNDjH', + schemaName: 'schema-name', + schemaVersion: '1.0', + }) + }) - expect(didFromSchemaId(schemaId)).toEqual('12345') + test('parses did:indy schema id', () => { + expect(parseSchemaId('did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH/anoncreds/v0/SCHEMA/schema-name/1.0')).toEqual( + { + didIdentifier: 'SDqTzbVuCowusqGBNbNDjH', + did: 'did:indy:bcovrin:test:SDqTzbVuCowusqGBNbNDjH', + schemaName: 'schema-name', + schemaVersion: '1.0', + namespace: 'bcovrin:test', + } + ) + }) }) - it('didFromCredentialDefinitionId should return the did from the credential definition id', () => { - const credentialDefinitionId = '12345:3:CL:420:someTag' + describe('parseCredentialDefinitionId', () => { + test('parses legacy credential definition id', () => { + expect(parseCredentialDefinitionId('TL1EaPFCZ8Si5aUrqScBDt:3:CL:10:TAG')).toEqual({ + did: 'TL1EaPFCZ8Si5aUrqScBDt', + didIdentifier: 'TL1EaPFCZ8Si5aUrqScBDt', + schemaSeqNo: '10', + tag: 'TAG', + }) + }) - expect(didFromCredentialDefinitionId(credentialDefinitionId)).toEqual('12345') + test('parses did:indy credential definition id', () => { + expect( + parseCredentialDefinitionId('did:indy:pool:localtest:TL1EaPFCZ8Si5aUrqScBDt/anoncreds/v0/CLAIM_DEF/10/TAG') + ).toEqual({ + didIdentifier: 'TL1EaPFCZ8Si5aUrqScBDt', + did: 'did:indy:pool:localtest:TL1EaPFCZ8Si5aUrqScBDt', + namespace: 'pool:localtest', + schemaSeqNo: '10', + tag: 'TAG', + }) + }) }) - it('didFromRevocationRegistryDefinitionId should return the did from the revocation registry id', () => { - const revocationRegistryId = '12345:3:CL:420:someTag' + describe('parseRevocationRegistryId', () => { + test('parses legacy revocation registry id', () => { + expect( + parseRevocationRegistryId('5nDyJVP1NrcPAttP3xwMB9:4:5nDyJVP1NrcPAttP3xwMB9:3:CL:56495:npdb:CL_ACCUM:TAG1') + ).toEqual({ + did: '5nDyJVP1NrcPAttP3xwMB9', + didIdentifier: '5nDyJVP1NrcPAttP3xwMB9', + schemaSeqNo: '56495', + credentialDefinitionTag: 'npdb', + revocationRegistryTag: 'TAG1', + }) + }) - expect(didFromRevocationRegistryDefinitionId(revocationRegistryId)).toEqual('12345') + test('parses did:indy revocation registry id', () => { + expect( + parseRevocationRegistryId('did:indy:sovrin:5nDyJVP1NrcPAttP3xwMB9/anoncreds/v0/REV_REG_DEF/56495/npdb/TAG1') + ).toEqual({ + namespace: 'sovrin', + didIdentifier: '5nDyJVP1NrcPAttP3xwMB9', + did: 'did:indy:sovrin:5nDyJVP1NrcPAttP3xwMB9', + schemaSeqNo: '56495', + credentialDefinitionTag: 'npdb', + revocationRegistryTag: 'TAG1', + }) + }) }) }) diff --git a/packages/indy-sdk/src/anoncreds/utils/__tests__/transform.test.ts b/packages/indy-sdk/src/anoncreds/utils/__tests__/transform.test.ts index 7930bfb2fb..f94060d8fd 100644 --- a/packages/indy-sdk/src/anoncreds/utils/__tests__/transform.test.ts +++ b/packages/indy-sdk/src/anoncreds/utils/__tests__/transform.test.ts @@ -12,7 +12,7 @@ describe('transform', () => { it('anonCredsSchemaFromIndySdk should return a valid anoncreds schema', () => { const schema: Schema = { attrNames: ['hello'], - id: '12345:2:Example Schema:1.0.0', + id: 'TL1EaPFCZ8Si5aUrqScBDt:2:Example Schema:1.0.0', name: 'Example Schema', seqNo: 150, ver: '1.0', @@ -21,24 +21,24 @@ describe('transform', () => { expect(anonCredsSchemaFromIndySdk(schema)).toEqual({ attrNames: ['hello'], - issuerId: '12345', + issuerId: 'TL1EaPFCZ8Si5aUrqScBDt', name: 'Example Schema', version: '1.0.0', }) }) it('indySdkSchemaFromAnonCreds should return a valid indy sdk schema', () => { - const schemaId = '12345:2:Example Schema:1.0.0' + const schemaId = 'TL1EaPFCZ8Si5aUrqScBDt:2:Example Schema:1.0.0' const schema: AnonCredsSchema = { attrNames: ['hello'], - issuerId: '12345', + issuerId: 'TL1EaPFCZ8Si5aUrqScBDt', name: 'Example Schema', version: '1.0.0', } expect(indySdkSchemaFromAnonCreds(schemaId, schema, 150)).toEqual({ attrNames: ['hello'], - id: '12345:2:Example Schema:1.0.0', + id: 'TL1EaPFCZ8Si5aUrqScBDt:2:Example Schema:1.0.0', name: 'Example Schema', seqNo: 150, ver: '1.0', @@ -48,7 +48,7 @@ describe('transform', () => { it('anonCredsCredentialDefinitionFromIndySdk should return a valid anoncreds credential definition', () => { const credDef: CredDef = { - id: '12345:3:CL:420:someTag', + id: 'TL1EaPFCZ8Si5aUrqScBDt:3:CL:420:someTag', schemaId: '8910:2:Example Schema:1.0.0', tag: 'someTag', type: 'CL', @@ -61,7 +61,7 @@ describe('transform', () => { } expect(anonCredsCredentialDefinitionFromIndySdk(credDef)).toEqual({ - issuerId: '12345', + issuerId: 'TL1EaPFCZ8Si5aUrqScBDt', schemaId: '8910:2:Example Schema:1.0.0', tag: 'someTag', type: 'CL', @@ -74,9 +74,9 @@ describe('transform', () => { }) it('indySdkCredentialDefinitionFromAnonCreds should return a valid indy sdk credential definition', () => { - const credentialDefinitionId = '12345:3:CL:420:someTag' + const credentialDefinitionId = 'TL1EaPFCZ8Si5aUrqScBDt:3:CL:420:someTag' const credentialDefinition: AnonCredsCredentialDefinition = { - issuerId: '12345', + issuerId: 'TL1EaPFCZ8Si5aUrqScBDt', schemaId: '8910:2:Example Schema:1.0.0', tag: 'someTag', type: 'CL', @@ -88,7 +88,7 @@ describe('transform', () => { } expect(indySdkCredentialDefinitionFromAnonCreds(credentialDefinitionId, credentialDefinition)).toEqual({ - id: '12345:3:CL:420:someTag', + id: 'TL1EaPFCZ8Si5aUrqScBDt:3:CL:420:someTag', schemaId: '8910:2:Example Schema:1.0.0', tag: 'someTag', type: 'CL', diff --git a/packages/indy-sdk/src/anoncreds/utils/identifiers.ts b/packages/indy-sdk/src/anoncreds/utils/identifiers.ts index 62d2650602..8300a7ea29 100644 --- a/packages/indy-sdk/src/anoncreds/utils/identifiers.ts +++ b/packages/indy-sdk/src/anoncreds/utils/identifiers.ts @@ -1,52 +1,151 @@ -export const legacyIndyIssuerIdRegex = /^[a-zA-Z0-9]{21,22}$/ -export const legacyIndySchemaIdRegex = /^[a-zA-Z0-9]{21,22}:2:.+:[0-9.]+$/ -export const legacyIndyCredentialDefinitionIdRegex = - /^[a-zA-Z0-9]{21,22}:3:CL:(([1-9][0-9]*)|([a-zA-Z0-9]{21,22}:2:.+:[0-9.]+)):(.+)?$/ -export const legacyIndyRevocationRegistryIdRegex = - /^[a-zA-Z0-9]{21,22}:4:[a-zA-Z0-9]{21,22}:3:CL:(([1-9][0-9]*)|([a-zA-Z0-9]{21,22}:2:.+:[0-9.]+))(:.+)?:CL_ACCUM:(.+$)/ +import { DID_INDY_REGEX } from '../../utils/did' -export const indySdkAnonCredsRegistryIdentifierRegex = new RegExp( - `${legacyIndyIssuerIdRegex.source}|${legacyIndySchemaIdRegex.source}|${legacyIndyCredentialDefinitionIdRegex.source}|${legacyIndyRevocationRegistryIdRegex.source}` +const didIndyAnonCredsBase = + /(?did:indy:(?((?:[a-z][_a-z0-9-]*)(?::[a-z][_a-z0-9-]*)?)):(?([1-9A-HJ-NP-Za-km-z]{21,22})))\/anoncreds\/v0/ + +// did:indy::/anoncreds/v0/SCHEMA// +const didIndySchemaIdRegex = new RegExp( + `^${didIndyAnonCredsBase.source}/SCHEMA/(?.+)/(?[0-9.]+)$` +) + +// :2:: +const legacyIndySchemaIdRegex = + /^(?(?[a-zA-Z0-9]{21,22})):2:(?.+):(?[0-9.]+)$/ + +// did:indy::/anoncreds/v0/CLAIM_DEF// +const didIndyCredentialDefinitionIdRegex = new RegExp( + `^${didIndyAnonCredsBase.source}/CLAIM_DEF/(?[1-9][0-9]*)/(?.+)$` +) + +// :3:CL:: +const legacyIndyCredentialDefinitionIdRegex = + /^(?(?[a-zA-Z0-9]{21,22})):3:CL:(?[1-9][0-9]*):(?.+)$/ + +// did:indy::/anoncreds/v0/REV_REG_DEF/// +const didIndyRevocationRegistryIdRegex = new RegExp( + `^${didIndyAnonCredsBase.source}/REV_REG_DEF/(?[1-9][0-9]*)/(?.+)/(?.+)$` ) -export function getIndySeqNoFromUnqualifiedCredentialDefinitionId(unqualifiedCredentialDefinitionId: string): number { - // 5nDyJVP1NrcPAttP3xwMB9:3:CL:56495:npbd - const [, , , seqNo] = unqualifiedCredentialDefinitionId.split(':') +// :4::3:CL::CL_ACCUM: +const legacyIndyRevocationRegistryIdRegex = + /^(?(?[a-zA-Z0-9]{21,22})):4:[a-zA-Z0-9]{21,22}:3:CL:(?[1-9][0-9]*):(?.+):CL_ACCUM:(?.+)$/ + +// combines both legacy and did:indy anoncreds identifiers and also the issuer id +const indySdkAnonCredsRegexes = [ + // NOTE: we only include the qualified issuer id here, as we don't support registering objects based on legacy issuer ids. + // you can still resolve using legacy issuer ids, but you need to use the full did:indy identifier when registering. + // As we find a matching anoncreds registry based on the issuerId only when creating an object, this will make sure + // it will throw an no registry found for identifier error. + // issuer id + DID_INDY_REGEX, + + // schema + didIndySchemaIdRegex, + legacyIndySchemaIdRegex, + + // credential definition + didIndyCredentialDefinitionIdRegex, + legacyIndyCredentialDefinitionIdRegex, + + // revocation registry + legacyIndyRevocationRegistryIdRegex, + didIndyRevocationRegistryIdRegex, +] + +export const indySdkAnonCredsRegistryIdentifierRegex = new RegExp( + indySdkAnonCredsRegexes.map((r) => r.source.replace(/(\?<[a-zA-Z]+>)?/g, '')).join('|') +) - return Number(seqNo) +export function getDidIndySchemaId(namespace: string, unqualifiedDid: string, name: string, version: string) { + return `did:indy:${namespace}:${unqualifiedDid}/anoncreds/v0/SCHEMA/${name}/${version}` } export function getLegacySchemaId(unqualifiedDid: string, name: string, version: string) { return `${unqualifiedDid}:2:${name}:${version}` } -export function getLegacyCredentialDefinitionId(unqualifiedDid: string, seqNo: number, tag: string) { +export function getLegacyCredentialDefinitionId(unqualifiedDid: string, seqNo: string | number, tag: string) { return `${unqualifiedDid}:3:CL:${seqNo}:${tag}` } -/** - * Extract did from schema id - */ -export function didFromSchemaId(schemaId: string) { - const [did] = schemaId.split(':') +export function getDidIndyCredentialDefinitionId( + namespace: string, + unqualifiedDid: string, + seqNo: string | number, + tag: string +) { + return `did:indy:${namespace}:${unqualifiedDid}/anoncreds/v0/CLAIM_DEF/${seqNo}/${tag}` +} + +// TZQuLp43UcYTdtc3HewcDz:4:TZQuLp43UcYTdtc3HewcDz:3:CL:98158:BaustellenzertifikateNU1:CL_ACCUM:1-100 +export function getLegacyRevocationRegistryId( + unqualifiedDid: string, + seqNo: string | number, + credentialDefinitionTag: string, + revocationRegistryTag: string +) { + return `${unqualifiedDid}:4:${unqualifiedDid}:3:CL:${seqNo}:${credentialDefinitionTag}:CL_ACCUM:${revocationRegistryTag}` +} + +export function getDidIndyRevocationRegistryId( + namespace: string, + unqualifiedDid: string, + seqNo: string | number, + credentialDefinitionTag: string, + revocationRegistryTag: string +) { + return `did:indy:${namespace}:${unqualifiedDid}/anoncreds/v0/REV_REG_DEF/${seqNo}/${credentialDefinitionTag}/${revocationRegistryTag}` +} - return did +interface ParsedSchemaId { + did: string + didIdentifier: string + schemaName: string + schemaVersion: string + namespace?: string } -/** - * Extract did from credential definition id - */ -export function didFromCredentialDefinitionId(credentialDefinitionId: string) { - const [did] = credentialDefinitionId.split(':') +export function parseSchemaId(schemaId: string) { + const match = schemaId.match(didIndySchemaIdRegex) ?? schemaId.match(legacyIndySchemaIdRegex) + + if (!match) throw new Error(`Invalid schema id: ${schemaId}`) + + return match.groups as unknown as ParsedSchemaId +} - return did +interface ParsedCredentialDefinitionId { + did: string + didIdentifier: string + schemaSeqNo: string + tag: string + namespace?: string } -/** - * Extract did from revocation registry definition id - */ -export function didFromRevocationRegistryDefinitionId(revocationRegistryId: string) { - const [did] = revocationRegistryId.split(':') +export function parseCredentialDefinitionId(credentialDefinitionId: string) { + const match = + credentialDefinitionId.match(didIndyCredentialDefinitionIdRegex) ?? + credentialDefinitionId.match(legacyIndyCredentialDefinitionIdRegex) + + if (!match) throw new Error(`Invalid credential definition id: ${credentialDefinitionId}`) + + return match.groups as unknown as ParsedCredentialDefinitionId +} + +interface ParsedRevocationRegistryId { + did: string + didIdentifier: string + schemaSeqNo: string + credentialDefinitionTag: string + revocationRegistryTag: string + namespace?: string +} + +export function parseRevocationRegistryId(revocationRegistryId: string) { + const match = + revocationRegistryId.match(didIndyRevocationRegistryIdRegex) ?? + revocationRegistryId.match(legacyIndyRevocationRegistryIdRegex) + + if (!match) throw new Error(`Invalid revocation registry id: ${revocationRegistryId}`) - return did + return match.groups as unknown as ParsedRevocationRegistryId } diff --git a/packages/indy-sdk/src/anoncreds/utils/transform.ts b/packages/indy-sdk/src/anoncreds/utils/transform.ts index e976d514e4..a993475349 100644 --- a/packages/indy-sdk/src/anoncreds/utils/transform.ts +++ b/packages/indy-sdk/src/anoncreds/utils/transform.ts @@ -6,12 +6,12 @@ import type { } from '@aries-framework/anoncreds' import type { CredDef, RevocReg, RevocRegDef, RevocRegDelta, Schema } from 'indy-sdk' -import { didFromCredentialDefinitionId, didFromRevocationRegistryDefinitionId, didFromSchemaId } from './identifiers' +import { parseCredentialDefinitionId, parseSchemaId } from './identifiers' export function anonCredsSchemaFromIndySdk(schema: Schema): AnonCredsSchema { - const issuerId = didFromSchemaId(schema.id) + const { did } = parseSchemaId(schema.id) return { - issuerId, + issuerId: did, name: schema.name, version: schema.version, attrNames: schema.attrNames, @@ -30,10 +30,10 @@ export function indySdkSchemaFromAnonCreds(schemaId: string, schema: AnonCredsSc } export function anonCredsCredentialDefinitionFromIndySdk(credentialDefinition: CredDef): AnonCredsCredentialDefinition { - const issuerId = didFromCredentialDefinitionId(credentialDefinition.id) + const { did } = parseCredentialDefinitionId(credentialDefinition.id) return { - issuerId, + issuerId: did, schemaId: credentialDefinition.schemaId, tag: credentialDefinition.tag, type: 'CL', @@ -55,25 +55,6 @@ export function indySdkCredentialDefinitionFromAnonCreds( } } -export function anonCredsRevocationRegistryDefinitionFromIndySdk( - revocationRegistryDefinition: RevocRegDef -): AnonCredsRevocationRegistryDefinition { - const issuerId = didFromRevocationRegistryDefinitionId(revocationRegistryDefinition.id) - - return { - issuerId, - credDefId: revocationRegistryDefinition.credDefId, - value: { - maxCredNum: revocationRegistryDefinition.value.maxCredNum, - publicKeys: revocationRegistryDefinition.value.publicKeys, - tailsHash: revocationRegistryDefinition.value.tailsHash, - tailsLocation: revocationRegistryDefinition.value.tailsLocation, - }, - tag: revocationRegistryDefinition.tag, - revocDefType: 'CL_ACCUM', - } -} - export function indySdkRevocationRegistryDefinitionFromAnonCreds( revocationRegistryDefinitionId: string, revocationRegistryDefinition: AnonCredsRevocationRegistryDefinition diff --git a/packages/indy-sdk/src/dids/IndySdkIndyDidRegistrar.ts b/packages/indy-sdk/src/dids/IndySdkIndyDidRegistrar.ts new file mode 100644 index 0000000000..77689dcde6 --- /dev/null +++ b/packages/indy-sdk/src/dids/IndySdkIndyDidRegistrar.ts @@ -0,0 +1,315 @@ +import type { IndyEndpointAttrib } from './didSovUtil' +import type { IndySdkPool } from '../ledger' +import type { IndySdk } from '../types' +import type { + AgentContext, + Buffer, + DidCreateOptions, + DidCreateResult, + DidDeactivateResult, + DidRegistrar, + DidUpdateResult, +} from '@aries-framework/core' +import type { NymRole } from 'indy-sdk' + +import { DidDocumentRole, DidRecord, DidRepository, KeyType, Key } from '@aries-framework/core' + +import { IndySdkError } from '../error' +import { isIndyError } from '../error/indyError' +import { IndySdkPoolService } from '../ledger' +import { IndySdkSymbol } from '../types' +import { assertIndySdkWallet } from '../utils/assertIndySdkWallet' +import { isLegacySelfCertifiedDid, legacyIndyDidFromPublicKeyBase58 } from '../utils/did' + +import { createKeyAgreementKey, indyDidDocumentFromDid, parseIndyDid, verificationKeyForIndyDid } from './didIndyUtil' +import { addServicesFromEndpointsAttrib } from './didSovUtil' + +export class IndySdkIndyDidRegistrar implements DidRegistrar { + public readonly supportedMethods = ['indy'] + + public async create(agentContext: AgentContext, options: IndySdkIndyDidCreateOptions): Promise { + const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) + const didRepository = agentContext.dependencyManager.resolve(DidRepository) + + const { alias, role, submitterDid, endpoints } = options.options + let did = options.did + let didIdentifier: string + let verificationKey: Key + const privateKey = options.secret?.privateKey + + if (did && privateKey) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `Only one of 'privateKey' or 'did' must be provided`, + }, + } + } + + try { + assertIndySdkWallet(agentContext.wallet) + + // Parse submitterDid and extract namespace based on the submitter did + const { namespace: submitterNamespace, id: submitterDidIdentifier } = parseIndyDid(submitterDid) + const submitterSigningKey = await verificationKeyForIndyDid(agentContext, submitterDid) + + // Only supports version 1 did identifier (which is same as did:sov) + if (did) { + if (!options.options.verkey) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'If a did is defined, a matching verkey must be provided', + }, + } + } + + const { namespace, id } = parseIndyDid(did) + didIdentifier = id + verificationKey = Key.fromPublicKeyBase58(options.options.verkey, KeyType.Ed25519) + + if (!isLegacySelfCertifiedDid(didIdentifier, options.options.verkey)) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `Did must be first 16 bytes of the the verkey base58 encoded.`, + }, + } + } + + if (submitterNamespace !== namespace) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `The submitter did uses namespace ${submitterNamespace} and the did to register uses namespace ${namespace}. Namespaces must match.`, + }, + } + } + } else { + // Create a new key and calculate did according to the rules for indy did method + verificationKey = await agentContext.wallet.createKey({ privateKey, keyType: KeyType.Ed25519 }) + didIdentifier = legacyIndyDidFromPublicKeyBase58(verificationKey.publicKeyBase58) + did = `did:indy:${submitterNamespace}:${didIdentifier}` + } + + const pool = indySdkPoolService.getPoolForNamespace(submitterNamespace) + await this.registerPublicDid( + agentContext, + pool, + submitterDidIdentifier, + submitterSigningKey, + didIdentifier, + verificationKey, + alias, + role + ) + + // Create did document + const didDocumentBuilder = indyDidDocumentFromDid(did, verificationKey.publicKeyBase58) + + // Add services if endpoints object was passed. + if (endpoints) { + const keyAgreementId = `${did}#key-agreement-1` + + await this.setEndpointsForDid(agentContext, pool, didIdentifier, verificationKey, endpoints) + + didDocumentBuilder + .addContext('https://w3id.org/security/suites/x25519-2019/v1') + .addVerificationMethod({ + controller: did, + id: keyAgreementId, + publicKeyBase58: createKeyAgreementKey(verificationKey.publicKeyBase58), + type: 'X25519KeyAgreementKey2019', + }) + .addKeyAgreement(keyAgreementId) + + // Process endpoint attrib following the same rules as for did:sov + addServicesFromEndpointsAttrib(didDocumentBuilder, did, endpoints, keyAgreementId) + } + + // Build did document. + const didDocument = didDocumentBuilder.build() + + // Save the did so we know we created it and can issue with it + const didRecord = new DidRecord({ + did, + role: DidDocumentRole.Created, + tags: { + recipientKeyFingerprints: didDocument.recipientKeys.map((key: Key) => key.fingerprint), + }, + }) + await didRepository.save(agentContext, didRecord) + + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did, + didDocument, + secret: { + // FIXME: the uni-registrar creates the seed in the registrar method + // if it doesn't exist so the seed can always be returned. Currently + // we can only return it if the seed was passed in by the user. Once + // we have a secure method for generating seeds we should use the same + // approach + privateKey: options.secret?.privateKey, + }, + }, + } + } catch (error) { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `unknownError: ${error.message}`, + }, + } + } + } + + public async update(): Promise { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notImplemented: updating did:indy not implemented yet`, + }, + } + } + + public async deactivate(): Promise { + return { + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notImplemented: deactivating did:indy not implemented yet`, + }, + } + } + + public async registerPublicDid( + agentContext: AgentContext, + pool: IndySdkPool, + unqualifiedSubmitterDid: string, + submitterSigningKey: Key, + unqualifiedDid: string, + signingKey: Key, + alias: string, + role?: NymRole + ) { + const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) + const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) + + try { + agentContext.config.logger.debug(`Register public did '${unqualifiedDid}' on ledger '${pool.didIndyNamespace}'`) + + const request = await indySdk.buildNymRequest( + unqualifiedSubmitterDid, + unqualifiedDid, + signingKey.publicKeyBase58, + alias, + role || null + ) + + const response = await indySdkPoolService.submitWriteRequest(agentContext, pool, request, submitterSigningKey) + + agentContext.config.logger.debug( + `Registered public did '${unqualifiedDid}' on ledger '${pool.didIndyNamespace}'`, + { + response, + } + ) + } catch (error) { + agentContext.config.logger.error( + `Error registering public did '${unqualifiedDid}' on ledger '${pool.didIndyNamespace}'`, + { + error, + unqualifiedSubmitterDid, + unqualifiedDid, + verkey: signingKey.publicKeyBase58, + alias, + role, + pool: pool.didIndyNamespace, + } + ) + + throw error + } + } + + public async setEndpointsForDid( + agentContext: AgentContext, + pool: IndySdkPool, + unqualifiedDid: string, + signingKey: Key, + endpoints: IndyEndpointAttrib + ): Promise { + const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) + const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) + + try { + agentContext.config.logger.debug( + `Set endpoints for did '${unqualifiedDid}' on ledger '${pool.didIndyNamespace}'`, + endpoints + ) + + const request = await indySdk.buildAttribRequest( + unqualifiedDid, + unqualifiedDid, + null, + { endpoint: endpoints }, + null + ) + + const response = await indySdkPoolService.submitWriteRequest(agentContext, pool, request, signingKey) + agentContext.config.logger.debug( + `Successfully set endpoints for did '${unqualifiedDid}' on ledger '${pool.didIndyNamespace}'`, + { + response, + endpoints, + } + ) + } catch (error) { + agentContext.config.logger.error( + `Error setting endpoints for did '${unqualifiedDid}' on ledger '${pool.didIndyNamespace}'`, + { + error, + unqualifiedDid, + endpoints, + } + ) + + throw isIndyError(error) ? new IndySdkError(error) : error + } + } +} + +export interface IndySdkIndyDidCreateOptions extends DidCreateOptions { + method: 'indy' + did?: string + // The indy sdk can only publish a very limited did document (what is mostly known as a legacy did:sov did) and thus we require everything + // needed to construct the did document to be passed through the options object. + didDocument?: never + options: { + alias: string + role?: NymRole + verkey?: string + endpoints?: IndyEndpointAttrib + submitterDid: string + } + secret?: { + privateKey?: Buffer + } +} diff --git a/packages/indy-sdk/src/dids/IndySdkIndyDidResolver.ts b/packages/indy-sdk/src/dids/IndySdkIndyDidResolver.ts new file mode 100644 index 0000000000..836ed8040b --- /dev/null +++ b/packages/indy-sdk/src/dids/IndySdkIndyDidResolver.ts @@ -0,0 +1,122 @@ +import type { IndyEndpointAttrib } from './didSovUtil' +import type { IndySdkPool } from '../ledger' +import type { IndySdk } from '../types' +import type { DidResolutionResult, DidResolver, AgentContext } from '@aries-framework/core' + +import { isIndyError, IndySdkError } from '../error' +import { IndySdkPoolService } from '../ledger/IndySdkPoolService' +import { IndySdkSymbol } from '../types' +import { getFullVerkey } from '../utils/did' + +import { createKeyAgreementKey, indyDidDocumentFromDid, parseIndyDid } from './didIndyUtil' +import { addServicesFromEndpointsAttrib } from './didSovUtil' + +export class IndySdkIndyDidResolver implements DidResolver { + public readonly supportedMethods = ['indy'] + + public async resolve(agentContext: AgentContext, did: string): Promise { + const didDocumentMetadata = {} + + try { + const { id: unqualifiedDid, namespace } = parseIndyDid(did) + + const poolService = agentContext.dependencyManager.resolve(IndySdkPoolService) + const pool = poolService.getPoolForNamespace(namespace) + + const nym = await this.getPublicDid(agentContext, pool, unqualifiedDid) + const endpoints = await this.getEndpointsForDid(agentContext, pool, unqualifiedDid) + + // For modern did:indy DIDs, we assume that GET_NYM is always a full verkey in base58. + // For backwards compatibility, we accept a shortened verkey and convert it using previous convention + const verkey = getFullVerkey(did, nym.verkey) + const builder = indyDidDocumentFromDid(did, verkey) + + // NOTE: we don't support the `diddocContent` field in the GET_NYM response using the indy-sdk. So if the did would have the `diddocContent` field + // we will ignore it without knowing if it is present. We may be able to extract the diddocContent from the GET_NYM response in the future, but need + // some dids registered with diddocContent to test with. + if (endpoints) { + const keyAgreementId = `${did}#key-agreement-1` + + builder + .addContext('https://w3id.org/security/suites/x25519-2019/v1') + .addVerificationMethod({ + controller: did, + id: keyAgreementId, + publicKeyBase58: createKeyAgreementKey(verkey), + type: 'X25519KeyAgreementKey2019', + }) + .addKeyAgreement(keyAgreementId) + addServicesFromEndpointsAttrib(builder, did, endpoints, keyAgreementId) + } + + return { + didDocument: builder.build(), + didDocumentMetadata, + didResolutionMetadata: { contentType: 'application/did+ld+json' }, + } + } catch (error) { + return { + didDocument: null, + didDocumentMetadata, + didResolutionMetadata: { + error: 'notFound', + message: `resolver_error: Unable to resolve did '${did}': ${error}`, + }, + } + } + } + + private async getPublicDid(agentContext: AgentContext, pool: IndySdkPool, unqualifiedDid: string) { + const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) + const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) + + const request = await indySdk.buildGetNymRequest(null, unqualifiedDid) + const response = await indySdkPoolService.submitReadRequest(pool, request) + + return await indySdk.parseGetNymResponse(response) + } + + private async getEndpointsForDid(agentContext: AgentContext, pool: IndySdkPool, unqualifiedDid: string) { + const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) + const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) + + try { + agentContext.config.logger.debug( + `Get endpoints for did '${unqualifiedDid}' from ledger '${pool.didIndyNamespace}'` + ) + + const request = await indySdk.buildGetAttribRequest(null, unqualifiedDid, 'endpoint', null, null) + + agentContext.config.logger.debug( + `Submitting get endpoint ATTRIB request for did '${unqualifiedDid}' to ledger '${pool.didIndyNamespace}'` + ) + const response = await indySdkPoolService.submitReadRequest(pool, request) + + if (!response.result.data) { + return null + } + + const endpoints = JSON.parse(response.result.data as string)?.endpoint as IndyEndpointAttrib + agentContext.config.logger.debug( + `Got endpoints '${JSON.stringify(endpoints)}' for did '${unqualifiedDid}' from ledger '${ + pool.didIndyNamespace + }'`, + { + response, + endpoints, + } + ) + + return endpoints + } catch (error) { + agentContext.config.logger.error( + `Error retrieving endpoints for did '${unqualifiedDid}' from ledger '${pool.didIndyNamespace}'`, + { + error, + } + ) + + throw isIndyError(error) ? new IndySdkError(error) : error + } + } +} diff --git a/packages/indy-sdk/src/dids/IndySdkSovDidRegistrar.ts b/packages/indy-sdk/src/dids/IndySdkSovDidRegistrar.ts deleted file mode 100644 index 86fb440e67..0000000000 --- a/packages/indy-sdk/src/dids/IndySdkSovDidRegistrar.ts +++ /dev/null @@ -1,302 +0,0 @@ -import type { IndyEndpointAttrib } from './didSovUtil' -import type { IndySdkPool } from '../ledger' -import type { IndySdk } from '../types' -import type { - AgentContext, - DidRegistrar, - DidCreateOptions, - DidCreateResult, - DidDeactivateResult, - DidUpdateResult, - Buffer, - Key, -} from '@aries-framework/core' -import type { NymRole } from 'indy-sdk' - -import { - DidsApi, - KeyType, - isValidPrivateKey, - DidDocumentRole, - DidRecord, - DidRepository, - getKeyFromVerificationMethod, -} from '@aries-framework/core' - -import { IndySdkError } from '../error' -import { isIndyError } from '../error/indyError' -import { IndySdkPoolService } from '../ledger' -import { IndySdkSymbol } from '../types' -import { indyDidFromPublicKeyBase58 } from '../utils/did' - -import { addServicesFromEndpointsAttrib, sovDidDocumentFromDid } from './didSovUtil' - -export class IndySdkSovDidRegistrar implements DidRegistrar { - public readonly supportedMethods = ['sov'] - - public async create(agentContext: AgentContext, options: IndySdkSovDidCreateOptions): Promise { - const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) - const didRepository = agentContext.dependencyManager.resolve(DidRepository) - - const { alias, role, submitterVerificationMethod, indyNamespace } = options.options - const privateKey = options.secret?.privateKey - - if (privateKey && !isValidPrivateKey(privateKey, KeyType.Ed25519)) { - return { - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: 'Invalid private key provided', - }, - } - } - - if (!submitterVerificationMethod.startsWith('did:sov:')) { - return { - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: 'Submitter did must be a valid did:sov did', - }, - } - } - - try { - const signingKey = await agentContext.wallet.createKey({ - privateKey, - keyType: KeyType.Ed25519, - }) - const verkey = signingKey.publicKeyBase58 - - const unqualifiedIndyDid = indyDidFromPublicKeyBase58(verkey) - - // FIXME: we should store the didDocument in the DidRecord so we don't have to fetch our own did - // from the ledger to know which key is associated with the did - const didsApi = agentContext.dependencyManager.resolve(DidsApi) - const didResult = await didsApi.resolve(submitterVerificationMethod) - - if (!didResult.didDocument) { - return { - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: `didNotFound: unable to resolve did ${submitterVerificationMethod}: ${didResult.didResolutionMetadata.message}`, - }, - } - } - - const verificationMethod = didResult.didDocument.dereferenceKey(submitterVerificationMethod) - const submitterSigningKey = getKeyFromVerificationMethod(verificationMethod) - - const qualifiedSovDid = `did:sov:${unqualifiedIndyDid}` - const [unqualifiedSubmitterDid] = submitterVerificationMethod.replace('did:sov:', '').split('#') - - const pool = indySdkPoolService.getPoolForNamespace(indyNamespace) - await this.registerPublicDid( - agentContext, - unqualifiedSubmitterDid, - submitterSigningKey, - unqualifiedIndyDid, - signingKey, - alias, - pool, - role - ) - - // Create did document - const didDocumentBuilder = sovDidDocumentFromDid(qualifiedSovDid, verkey) - - // Add services if endpoints object was passed. - if (options.options.endpoints) { - await this.setEndpointsForDid(agentContext, unqualifiedIndyDid, signingKey, options.options.endpoints, pool) - addServicesFromEndpointsAttrib( - didDocumentBuilder, - qualifiedSovDid, - options.options.endpoints, - `${qualifiedSovDid}#key-agreement-1` - ) - } - - // Build did document. - const didDocument = didDocumentBuilder.build() - - const didIndyNamespace = pool.config.indyNamespace - const qualifiedIndyDid = `did:indy:${didIndyNamespace}:${unqualifiedIndyDid}` - - // Save the did so we know we created it and can issue with it - const didRecord = new DidRecord({ - did: qualifiedSovDid, - role: DidDocumentRole.Created, - tags: { - recipientKeyFingerprints: didDocument.recipientKeys.map((key: Key) => key.fingerprint), - qualifiedIndyDid, - }, - }) - await didRepository.save(agentContext, didRecord) - - return { - didDocumentMetadata: { - qualifiedIndyDid, - }, - didRegistrationMetadata: { - didIndyNamespace, - }, - didState: { - state: 'finished', - did: qualifiedSovDid, - didDocument, - secret: { - // FIXME: the uni-registrar creates the seed in the registrar method - // if it doesn't exist so the seed can always be returned. Currently - // we can only return it if the seed was passed in by the user. Once - // we have a secure method for generating seeds we should use the same - // approach - privateKey: options.secret?.privateKey, - }, - }, - } - } catch (error) { - return { - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: `unknownError: ${error.message}`, - }, - } - } - } - - public async update(): Promise { - return { - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: `notImplemented: updating did:sov not implemented yet`, - }, - } - } - - public async deactivate(): Promise { - return { - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: `notImplemented: deactivating did:sov not implemented yet`, - }, - } - } - - public async registerPublicDid( - agentContext: AgentContext, - submitterDid: string, - submitterSigningKey: Key, - targetDid: string, - signingKey: Key, - alias: string, - pool: IndySdkPool, - role?: NymRole - ) { - const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) - const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) - - try { - agentContext.config.logger.debug(`Register public did '${targetDid}' on ledger '${pool.didIndyNamespace}'`) - - const request = await indySdk.buildNymRequest( - submitterDid, - targetDid, - signingKey.publicKeyBase58, - alias, - role || null - ) - - const response = await indySdkPoolService.submitWriteRequest(agentContext, pool, request, submitterSigningKey) - - agentContext.config.logger.debug(`Registered public did '${targetDid}' on ledger '${pool.didIndyNamespace}'`, { - response, - }) - - return targetDid - } catch (error) { - agentContext.config.logger.error( - `Error registering public did '${targetDid}' on ledger '${pool.didIndyNamespace}'`, - { - error, - submitterDid, - targetDid, - verkey: signingKey.publicKeyBase58, - alias, - role, - pool: pool.didIndyNamespace, - } - ) - - throw error - } - } - - public async setEndpointsForDid( - agentContext: AgentContext, - did: string, - signingKey: Key, - endpoints: IndyEndpointAttrib, - pool: IndySdkPool - ): Promise { - const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) - const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) - - try { - agentContext.config.logger.debug(`Set endpoints for did '${did}' on ledger '${pool.didIndyNamespace}'`, endpoints) - - const request = await indySdk.buildAttribRequest(did, did, null, { endpoint: endpoints }, null) - - const response = await indySdkPoolService.submitWriteRequest(agentContext, pool, request, signingKey) - agentContext.config.logger.debug( - `Successfully set endpoints for did '${did}' on ledger '${pool.didIndyNamespace}'`, - { - response, - endpoints, - } - ) - } catch (error) { - agentContext.config.logger.error( - `Error setting endpoints for did '${did}' on ledger '${pool.didIndyNamespace}'`, - { - error, - did, - endpoints, - } - ) - - throw isIndyError(error) ? new IndySdkError(error) : error - } - } -} - -export interface IndySdkSovDidCreateOptions extends DidCreateOptions { - method: 'sov' - did?: undefined - // As did:sov is so limited, we require everything needed to construct the did document to be passed - // through the options object. Once we support did:indy we can allow the didDocument property. - didDocument?: never - options: { - alias: string - role?: NymRole - endpoints?: IndyEndpointAttrib - indyNamespace?: string - submitterVerificationMethod: string - } - secret?: { - privateKey?: Buffer - } -} - -// Update and Deactivate not supported for did:sov -export type IndySdkSovDidUpdateOptions = never -export type IndySdkSovDidDeactivateOptions = never diff --git a/packages/indy-sdk/src/dids/IndySdkSovDidResolver.ts b/packages/indy-sdk/src/dids/IndySdkSovDidResolver.ts index 98007e5166..bbbeec71f8 100644 --- a/packages/indy-sdk/src/dids/IndySdkSovDidResolver.ts +++ b/packages/indy-sdk/src/dids/IndySdkSovDidResolver.ts @@ -1,4 +1,5 @@ import type { IndyEndpointAttrib } from './didSovUtil' +import type { IndySdkPool } from '../ledger' import type { IndySdk } from '../types' import type { DidResolutionResult, ParsedDid, DidResolver, AgentContext } from '@aries-framework/core' @@ -15,8 +16,10 @@ export class IndySdkSovDidResolver implements DidResolver { const didDocumentMetadata = {} try { - const nym = await this.getPublicDid(agentContext, parsed.id) - const endpoints = await this.getEndpointsForDid(agentContext, parsed.id) + const poolService = agentContext.dependencyManager.resolve(IndySdkPoolService) + const { pool, nymResponse } = await poolService.getPoolForDid(agentContext, parsed.id) + const nym = nymResponse ?? (await this.getPublicDid(agentContext, pool, parsed.id)) + const endpoints = await this.getEndpointsForDid(agentContext, pool, parsed.id) const keyAgreementId = `${parsed.did}#key-agreement-1` const builder = sovDidDocumentFromDid(parsed.did, nym.verkey) @@ -39,21 +42,20 @@ export class IndySdkSovDidResolver implements DidResolver { } } - private async getPublicDid(agentContext: AgentContext, did: string) { + private async getPublicDid(agentContext: AgentContext, pool: IndySdkPool, did: string) { const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) + const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) - // Getting the pool for a did also retrieves the DID. We can just use that - const { did: didResponse } = await indySdkPoolService.getPoolForDid(agentContext, did) + const request = await indySdk.buildGetNymRequest(null, did) + const response = await indySdkPoolService.submitReadRequest(pool, request) - return didResponse + return await indySdk.parseGetNymResponse(response) } - private async getEndpointsForDid(agentContext: AgentContext, did: string) { + private async getEndpointsForDid(agentContext: AgentContext, pool: IndySdkPool, did: string) { const indySdk = agentContext.dependencyManager.resolve(IndySdkSymbol) const indySdkPoolService = agentContext.dependencyManager.resolve(IndySdkPoolService) - const { pool } = await indySdkPoolService.getPoolForDid(agentContext, did) - try { agentContext.config.logger.debug(`Get endpoints for did '${did}' from ledger '${pool.didIndyNamespace}'`) diff --git a/packages/indy-sdk/src/dids/__tests__/IndySdkIndyDidRegistrar.test.ts b/packages/indy-sdk/src/dids/__tests__/IndySdkIndyDidRegistrar.test.ts new file mode 100644 index 0000000000..4d4390bd24 --- /dev/null +++ b/packages/indy-sdk/src/dids/__tests__/IndySdkIndyDidRegistrar.test.ts @@ -0,0 +1,494 @@ +import type { IndySdkPool } from '../../ledger/IndySdkPool' +import type { DidRecord, RecordSavedEvent } from '@aries-framework/core' + +import { + SigningProviderRegistry, + DidsApi, + DidDocument, + VerificationMethod, + KeyType, + Key, + TypedArrayEncoder, + DidRepository, + JsonTransformer, + DidDocumentRole, + EventEmitter, + RepositoryEventTypes, +} from '@aries-framework/core' +import { Subject } from 'rxjs' + +import { InMemoryStorageService } from '../../../../../tests/InMemoryStorageService' +import { mockFunction, getAgentConfig, getAgentContext, agentDependencies, indySdk } from '../../../../core/tests' +import { IndySdkPoolService } from '../../ledger/IndySdkPoolService' +import { IndySdkWallet } from '../../wallet' +import { IndySdkIndyDidRegistrar } from '../IndySdkIndyDidRegistrar' + +jest.mock('../../ledger/IndySdkPoolService') +const IndySdkPoolServiceMock = IndySdkPoolService as jest.Mock +const indySdkPoolServiceMock = new IndySdkPoolServiceMock() + +const pool = { + config: { indyNamespace: 'pool1' }, +} as IndySdkPool +mockFunction(indySdkPoolServiceMock.getPoolForNamespace).mockReturnValue(pool) + +const agentConfig = getAgentConfig('IndySdkIndyDidRegistrar') +const wallet = new IndySdkWallet(indySdk, agentConfig.logger, new SigningProviderRegistry([])) + +jest + .spyOn(wallet, 'createKey') + .mockResolvedValue(Key.fromPublicKeyBase58('E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', KeyType.Ed25519)) +const storageService = new InMemoryStorageService() +const eventEmitter = new EventEmitter(agentDependencies, new Subject()) +const didRepository = new DidRepository(storageService, eventEmitter) + +const agentContext = getAgentContext({ + wallet, + registerInstances: [ + [DidRepository, didRepository], + [IndySdkPoolService, indySdkPoolServiceMock], + [ + DidsApi, + { + resolve: jest.fn().mockResolvedValue({ + didDocument: new DidDocument({ + id: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', + authentication: [ + new VerificationMethod({ + id: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg#verkey', + type: 'Ed25519VerificationKey2018', + controller: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', + publicKeyBase58: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', + }), + ], + }), + }), + }, + ], + ], + agentConfig, +}) + +const indySdkIndyDidRegistrar = new IndySdkIndyDidRegistrar() + +describe('IndySdkIndyDidRegistrar', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + test('returns an error state if both did and privateKey are provided', async () => { + const result = await indySdkIndyDidRegistrar.create(agentContext, { + method: 'indy', + did: 'did:indy:pool1:did-value', + options: { + submitterDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', + alias: 'Hello', + }, + secret: { + privateKey: TypedArrayEncoder.fromString('key'), + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `Only one of 'privateKey' or 'did' must be provided`, + }, + }) + }) + + test('returns an error state if the submitter did is not a valid did:indy did', async () => { + const result = await indySdkIndyDidRegistrar.create(agentContext, { + method: 'indy', + options: { + submitterDid: 'BzCbsNYhMrjHiqZDTUASHg', + alias: 'Hello', + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'unknownError: BzCbsNYhMrjHiqZDTUASHg is not a valid did:indy did', + }, + }) + }) + + test('returns an error state if did is provided, but it is not a valid did:indy did', async () => { + const result = await indySdkIndyDidRegistrar.create(agentContext, { + method: 'indy', + did: 'BzCbsNYhMrjHiqZDTUASHg', + options: { + submitterDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', + verkey: 'verkey', + alias: 'Hello', + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'unknownError: BzCbsNYhMrjHiqZDTUASHg is not a valid did:indy did', + }, + }) + }) + + test('returns an error state if did is provided, but no verkey', async () => { + const result = await indySdkIndyDidRegistrar.create(agentContext, { + method: 'indy', + did: 'BzCbsNYhMrjHiqZDTUASHg', + options: { + submitterDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', + alias: 'Hello', + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'If a did is defined, a matching verkey must be provided', + }, + }) + }) + + test('returns an error state if did and verkey are provided, but the did is not self certifying', async () => { + const result = await indySdkIndyDidRegistrar.create(agentContext, { + method: 'indy', + did: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', + options: { + submitterDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', + verkey: 'verkey', + alias: 'Hello', + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: 'Did must be first 16 bytes of the the verkey base58 encoded.', + }, + }) + }) + + test('returns an error state if did is provided, but does not match with the namespace from the submitterDid', async () => { + const result = await indySdkIndyDidRegistrar.create(agentContext, { + method: 'indy', + did: 'did:indy:pool2:R1xKJw17sUoXhejEpugMYJ', + options: { + submitterDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', + verkey: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', + alias: 'Hello', + }, + }) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: + 'The submitter did uses namespace pool1 and the did to register uses namespace pool2. Namespaces must match.', + }, + }) + }) + + test('creates a did:indy document without services', async () => { + const privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') + + const registerPublicDidSpy = jest.spyOn(indySdkIndyDidRegistrar, 'registerPublicDid') + registerPublicDidSpy.mockImplementationOnce(() => Promise.resolve()) + + const result = await indySdkIndyDidRegistrar.create(agentContext, { + method: 'indy', + options: { + alias: 'Hello', + submitterDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', + role: 'STEWARD', + }, + secret: { + privateKey, + }, + }) + expect(registerPublicDidSpy).toHaveBeenCalledWith( + agentContext, + pool, + // Unqualified submitter did + 'BzCbsNYhMrjHiqZDTUASHg', + // submitter signing key, + expect.any(Key), + // Unqualified created indy did + 'R1xKJw17sUoXhejEpugMYJ', + // Verkey + expect.any(Key), + // Alias + 'Hello', + // Role + 'STEWARD' + ) + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ', + didDocument: { + '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/suites/ed25519-2018/v1'], + id: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ', + verificationMethod: [ + { + id: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#verkey', + type: 'Ed25519VerificationKey2018', + controller: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ', + publicKeyBase58: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', + }, + ], + authentication: ['did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#verkey'], + assertionMethod: undefined, + keyAgreement: undefined, + }, + secret: { + privateKey, + }, + }, + }) + }) + + test('creates a did:indy document by passing did', async () => { + const registerPublicDidSpy = jest.spyOn(indySdkIndyDidRegistrar, 'registerPublicDid') + registerPublicDidSpy.mockImplementationOnce(() => Promise.resolve()) + + const result = await indySdkIndyDidRegistrar.create(agentContext, { + method: 'indy', + did: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ', + options: { + verkey: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', + alias: 'Hello', + submitterDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', + role: 'STEWARD', + }, + secret: {}, + }) + expect(registerPublicDidSpy).toHaveBeenCalledWith( + agentContext, + pool, + // Unqualified submitter did + 'BzCbsNYhMrjHiqZDTUASHg', + // submitter signing key, + expect.any(Key), + // Unqualified created indy did + 'R1xKJw17sUoXhejEpugMYJ', + // Verkey + expect.any(Key), + // Alias + 'Hello', + // Role + 'STEWARD' + ) + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ', + didDocument: { + '@context': ['https://w3id.org/did/v1', 'https://w3id.org/security/suites/ed25519-2018/v1'], + id: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ', + verificationMethod: [ + { + id: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#verkey', + type: 'Ed25519VerificationKey2018', + controller: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ', + publicKeyBase58: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', + }, + ], + authentication: ['did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#verkey'], + assertionMethod: undefined, + keyAgreement: undefined, + }, + secret: {}, + }, + }) + }) + + test('creates a did:indy document with services', async () => { + const privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') + + const registerPublicDidSpy = jest.spyOn(indySdkIndyDidRegistrar, 'registerPublicDid') + registerPublicDidSpy.mockImplementationOnce(() => Promise.resolve()) + + const setEndpointsForDidSpy = jest.spyOn(indySdkIndyDidRegistrar, 'setEndpointsForDid') + setEndpointsForDidSpy.mockImplementationOnce(() => Promise.resolve(undefined)) + + const result = await indySdkIndyDidRegistrar.create(agentContext, { + method: 'indy', + options: { + alias: 'Hello', + submitterDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', + role: 'STEWARD', + endpoints: { + endpoint: 'https://example.com/endpoint', + routingKeys: ['key-1'], + types: ['DIDComm', 'did-communication', 'endpoint'], + }, + }, + secret: { + privateKey, + }, + }) + + expect(registerPublicDidSpy).toHaveBeenCalledWith( + agentContext, + pool, + // Unqualified submitter did + 'BzCbsNYhMrjHiqZDTUASHg', + // submitter signing key, + expect.any(Key), + // Unqualified created indy did + 'R1xKJw17sUoXhejEpugMYJ', + // Verkey + expect.any(Key), + // Alias + 'Hello', + // Role + 'STEWARD' + ) + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'finished', + did: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ', + didDocument: { + '@context': [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + 'https://didcomm.org/messaging/contexts/v2', + ], + id: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ', + verificationMethod: [ + { + id: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#verkey', + type: 'Ed25519VerificationKey2018', + controller: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ', + publicKeyBase58: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', + }, + { + id: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#key-agreement-1', + type: 'X25519KeyAgreementKey2019', + controller: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ', + publicKeyBase58: 'Fbv17ZbnUSbafsiUBJbdGeC62M8v8GEscVMMcE59mRPt', + }, + ], + service: [ + { + id: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#endpoint', + serviceEndpoint: 'https://example.com/endpoint', + type: 'endpoint', + }, + { + id: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#did-communication', + serviceEndpoint: 'https://example.com/endpoint', + type: 'did-communication', + priority: 0, + recipientKeys: ['did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#key-agreement-1'], + routingKeys: ['key-1'], + accept: ['didcomm/aip2;env=rfc19'], + }, + { + id: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#didcomm-1', + serviceEndpoint: 'https://example.com/endpoint', + type: 'DIDComm', + routingKeys: ['key-1'], + accept: ['didcomm/v2'], + }, + ], + authentication: ['did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#verkey'], + assertionMethod: undefined, + keyAgreement: ['did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#key-agreement-1'], + }, + secret: { + privateKey, + }, + }, + }) + }) + + test('stores the did document', async () => { + const privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') + + const registerPublicDidSpy = jest.spyOn(indySdkIndyDidRegistrar, 'registerPublicDid') + registerPublicDidSpy.mockImplementationOnce(() => Promise.resolve()) + + const setEndpointsForDidSpy = jest.spyOn(indySdkIndyDidRegistrar, 'setEndpointsForDid') + setEndpointsForDidSpy.mockImplementationOnce(() => Promise.resolve(undefined)) + + const saveCalled = jest.fn() + eventEmitter.on>(RepositoryEventTypes.RecordSaved, saveCalled) + + await indySdkIndyDidRegistrar.create(agentContext, { + method: 'indy', + options: { + alias: 'Hello', + submitterDid: 'did:indy:pool1:BzCbsNYhMrjHiqZDTUASHg', + role: 'STEWARD', + endpoints: { + endpoint: 'https://example.com/endpoint', + routingKeys: ['key-1'], + types: ['DIDComm', 'did-communication', 'endpoint'], + }, + }, + secret: { + privateKey, + }, + }) + + expect(saveCalled).toHaveBeenCalledTimes(1) + const [saveEvent] = saveCalled.mock.calls[0] + + expect(saveEvent.payload.record).toMatchObject({ + did: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ', + role: DidDocumentRole.Created, + _tags: { + recipientKeyFingerprints: ['z6LSrH6AdsQeZuKKmG6Ehx7abEQZsVg2psR2VU536gigUoAe'], + }, + didDocument: undefined, + }) + }) + + test('returns an error state when calling update', async () => { + const result = await indySdkIndyDidRegistrar.update() + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notImplemented: updating did:indy not implemented yet`, + }, + }) + }) + + test('returns an error state when calling deactivate', async () => { + const result = await indySdkIndyDidRegistrar.deactivate() + + expect(result).toEqual({ + didDocumentMetadata: {}, + didRegistrationMetadata: {}, + didState: { + state: 'failed', + reason: `notImplemented: deactivating did:indy not implemented yet`, + }, + }) + }) +}) diff --git a/packages/indy-sdk/src/dids/__tests__/IndySdkIndyDidResolver.test.ts b/packages/indy-sdk/src/dids/__tests__/IndySdkIndyDidResolver.test.ts new file mode 100644 index 0000000000..7c4f294286 --- /dev/null +++ b/packages/indy-sdk/src/dids/__tests__/IndySdkIndyDidResolver.test.ts @@ -0,0 +1,127 @@ +import type { IndySdkPool } from '../../ledger' +import type { IndyEndpointAttrib } from '../didSovUtil' +import type { GetNymResponse } from 'indy-sdk' + +import { SigningProviderRegistry, JsonTransformer } from '@aries-framework/core' +import indySdk from 'indy-sdk' + +import { mockFunction, getAgentConfig, getAgentContext } from '../../../../core/tests/helpers' +import { IndySdkPoolService } from '../../ledger/IndySdkPoolService' +import { IndySdkSymbol } from '../../types' +import { IndySdkWallet } from '../../wallet' +import { IndySdkIndyDidResolver } from '../IndySdkIndyDidResolver' + +import didIndyPool1R1xKJw17sUoXhejEpugMYJFixture from './__fixtures__/didIndyPool1R1xKJw17sUoXhejEpugMYJ.json' +import didIndyPool1WJz9mHyW9BZksioQnRsrAoFixture from './__fixtures__/didIndyPool1WJz9mHyW9BZksioQnRsrAo.json' + +jest.mock('../../ledger/IndySdkPoolService') +const IndySdkPoolServiceMock = IndySdkPoolService as jest.Mock +const indySdkPoolServiceMock = new IndySdkPoolServiceMock() + +mockFunction(indySdkPoolServiceMock.getPoolForNamespace).mockReturnValue({ + config: { indyNamespace: 'pool1' }, +} as IndySdkPool) + +const agentConfig = getAgentConfig('IndySdkIndyDidResolver') + +const wallet = new IndySdkWallet(indySdk, agentConfig.logger, new SigningProviderRegistry([])) + +const agentContext = getAgentContext({ + wallet, + agentConfig, + registerInstances: [ + [IndySdkPoolService, indySdkPoolServiceMock], + [IndySdkSymbol, indySdk], + ], +}) + +const indySdkSovDidResolver = new IndySdkIndyDidResolver() + +describe('IndySdkIndyDidResolver', () => { + it('should correctly resolve a did:indy document', async () => { + const did = 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ' + + const nymResponse: GetNymResponse = { + did: 'R1xKJw17sUoXhejEpugMYJ', + verkey: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', + role: 'ENDORSER', + } + + const endpoints: IndyEndpointAttrib = { + endpoint: 'https://ssi.com', + profile: 'https://profile.com', + hub: 'https://hub.com', + } + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + jest.spyOn(indySdkSovDidResolver, 'getPublicDid').mockResolvedValue(nymResponse) + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + jest.spyOn(indySdkSovDidResolver, 'getEndpointsForDid').mockResolvedValue(endpoints) + + const result = await indySdkSovDidResolver.resolve(agentContext, did) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocument: didIndyPool1R1xKJw17sUoXhejEpugMYJFixture, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) + + it('should resolve a did:indy document with routingKeys and types entries in the attrib', async () => { + const did = 'did:indy:pool1:WJz9mHyW9BZksioQnRsrAo' + + const nymResponse: GetNymResponse = { + did: 'WJz9mHyW9BZksioQnRsrAo', + verkey: 'GyYtYWU1vjwd5PFJM4VSX5aUiSV3TyZMuLBJBTQvfdF8', + role: 'ENDORSER', + } + + const endpoints: IndyEndpointAttrib = { + endpoint: 'https://agent.com', + types: ['endpoint', 'did-communication', 'DIDComm'], + routingKeys: ['routingKey1', 'routingKey2'], + } + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + jest.spyOn(indySdkSovDidResolver, 'getPublicDid').mockResolvedValue(nymResponse) + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + jest.spyOn(indySdkSovDidResolver, 'getEndpointsForDid').mockResolvedValue(endpoints) + + const result = await indySdkSovDidResolver.resolve(agentContext, did) + + expect(JsonTransformer.toJSON(result)).toMatchObject({ + didDocument: didIndyPool1WJz9mHyW9BZksioQnRsrAoFixture, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) + + it('should return did resolution metadata with error if the indy ledger service throws an error', async () => { + const did = 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ' + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + jest.spyOn(indySdkSovDidResolver, 'getPublicDid').mockRejectedValue(new Error('Error retrieving did')) + + const result = await indySdkSovDidResolver.resolve(agentContext, did) + + expect(result).toMatchObject({ + didDocument: null, + didDocumentMetadata: {}, + didResolutionMetadata: { + error: 'notFound', + message: `resolver_error: Unable to resolve did 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ': Error: Error retrieving did`, + }, + }) + }) +}) diff --git a/packages/indy-sdk/src/dids/__tests__/IndySdkSovDidRegistrar.test.ts b/packages/indy-sdk/src/dids/__tests__/IndySdkSovDidRegistrar.test.ts deleted file mode 100644 index 2f63d2a91a..0000000000 --- a/packages/indy-sdk/src/dids/__tests__/IndySdkSovDidRegistrar.test.ts +++ /dev/null @@ -1,372 +0,0 @@ -import type { IndySdkPool } from '../../ledger/IndySdkPool' -import type { Wallet, DidRecord, RecordSavedEvent } from '@aries-framework/core' - -import { - DidsApi, - DidDocument, - VerificationMethod, - KeyType, - Key, - TypedArrayEncoder, - DidRepository, - JsonTransformer, - DidDocumentRole, - EventEmitter, - RepositoryEventTypes, -} from '@aries-framework/core' -import { Subject } from 'rxjs' - -import { InMemoryStorageService } from '../../../../../tests/InMemoryStorageService' -import { mockFunction, getAgentConfig, getAgentContext, agentDependencies } from '../../../../core/tests' -import { IndySdkPoolService } from '../../ledger/IndySdkPoolService' -import { IndySdkSovDidRegistrar } from '../IndySdkSovDidRegistrar' - -jest.mock('../../ledger/IndySdkPoolService') -const IndySdkPoolServiceMock = IndySdkPoolService as jest.Mock -const indySdkPoolServiceMock = new IndySdkPoolServiceMock() - -mockFunction(indySdkPoolServiceMock.getPoolForNamespace).mockReturnValue({ - config: { indyNamespace: 'pool1' }, -} as IndySdkPool) - -const agentConfig = getAgentConfig('IndySdkSovDidRegistrar') - -const wallet = { - createKey: jest - .fn() - .mockResolvedValue(Key.fromPublicKeyBase58('E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', KeyType.Ed25519)), -} as unknown as Wallet -const storageService = new InMemoryStorageService() -const eventEmitter = new EventEmitter(agentDependencies, new Subject()) -const didRepository = new DidRepository(storageService, eventEmitter) - -const agentContext = getAgentContext({ - wallet, - registerInstances: [ - [DidRepository, didRepository], - [IndySdkPoolService, indySdkPoolServiceMock], - [ - DidsApi, - { - resolve: jest.fn().mockResolvedValue({ - didDocument: new DidDocument({ - id: 'did:sov:BzCbsNYhMrjHiqZDTUASHg', - authentication: [ - new VerificationMethod({ - id: 'did:sov:BzCbsNYhMrjHiqZDTUASHg#key-1', - type: 'Ed25519VerificationKey2018', - controller: 'did:sov:BzCbsNYhMrjHiqZDTUASHg', - publicKeyBase58: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', - }), - ], - }), - }), - }, - ], - ], - agentConfig, -}) - -const indySdkSovDidRegistrar = new IndySdkSovDidRegistrar() - -describe('IndySdkSovDidRegistrar', () => { - afterEach(() => { - jest.clearAllMocks() - }) - - it('should return an error state if an invalid private key is provided', async () => { - const result = await indySdkSovDidRegistrar.create(agentContext, { - method: 'sov', - - options: { - submitterVerificationMethod: 'did:sov:BzCbsNYhMrjHiqZDTUASHg#key-1', - alias: 'Hello', - }, - secret: { - privateKey: TypedArrayEncoder.fromString('invalid'), - }, - }) - - expect(JsonTransformer.toJSON(result)).toMatchObject({ - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: 'Invalid private key provided', - }, - }) - }) - - it('should return an error state if the submitter did is not qualified with did:sov', async () => { - const result = await indySdkSovDidRegistrar.create(agentContext, { - method: 'sov', - options: { - submitterVerificationMethod: 'BzCbsNYhMrjHiqZDTUASHg', - alias: 'Hello', - }, - }) - - expect(JsonTransformer.toJSON(result)).toMatchObject({ - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: 'Submitter did must be a valid did:sov did', - }, - }) - }) - - it('should correctly create a did:sov document without services', async () => { - const privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') - - const registerPublicDidSpy = jest.spyOn(indySdkSovDidRegistrar, 'registerPublicDid') - registerPublicDidSpy.mockImplementationOnce(() => Promise.resolve('R1xKJw17sUoXhejEpugMYJ')) - - const result = await indySdkSovDidRegistrar.create(agentContext, { - method: 'sov', - options: { - alias: 'Hello', - submitterVerificationMethod: 'did:sov:BzCbsNYhMrjHiqZDTUASHg#key-1', - role: 'STEWARD', - }, - secret: { - privateKey, - }, - }) - expect(registerPublicDidSpy).toHaveBeenCalledWith( - agentContext, - // Unqualified submitter did - 'BzCbsNYhMrjHiqZDTUASHg', - // submitter signing key, - expect.any(Key), - // Unqualified created indy did - 'R1xKJw17sUoXhejEpugMYJ', - // Verkey - expect.any(Key), - // Alias - 'Hello', - // Pool - { config: { indyNamespace: 'pool1' } }, - // Role - 'STEWARD' - ) - expect(JsonTransformer.toJSON(result)).toMatchObject({ - didDocumentMetadata: { - qualifiedIndyDid: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ', - }, - didRegistrationMetadata: { - didIndyNamespace: 'pool1', - }, - didState: { - state: 'finished', - did: 'did:sov:R1xKJw17sUoXhejEpugMYJ', - didDocument: { - '@context': [ - 'https://w3id.org/did/v1', - 'https://w3id.org/security/suites/ed25519-2018/v1', - 'https://w3id.org/security/suites/x25519-2019/v1', - ], - id: 'did:sov:R1xKJw17sUoXhejEpugMYJ', - verificationMethod: [ - { - id: 'did:sov:R1xKJw17sUoXhejEpugMYJ#key-1', - type: 'Ed25519VerificationKey2018', - controller: 'did:sov:R1xKJw17sUoXhejEpugMYJ', - publicKeyBase58: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', - }, - { - id: 'did:sov:R1xKJw17sUoXhejEpugMYJ#key-agreement-1', - type: 'X25519KeyAgreementKey2019', - controller: 'did:sov:R1xKJw17sUoXhejEpugMYJ', - publicKeyBase58: 'Fbv17ZbnUSbafsiUBJbdGeC62M8v8GEscVMMcE59mRPt', - }, - ], - authentication: ['did:sov:R1xKJw17sUoXhejEpugMYJ#key-1'], - assertionMethod: ['did:sov:R1xKJw17sUoXhejEpugMYJ#key-1'], - keyAgreement: ['did:sov:R1xKJw17sUoXhejEpugMYJ#key-agreement-1'], - }, - secret: { - privateKey, - }, - }, - }) - }) - - it('should correctly create a did:sov document with services', async () => { - const privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') - - const registerPublicDidSpy = jest.spyOn(indySdkSovDidRegistrar, 'registerPublicDid') - registerPublicDidSpy.mockImplementationOnce(() => Promise.resolve('R1xKJw17sUoXhejEpugMYJ')) - - const setEndpointsForDidSpy = jest.spyOn(indySdkSovDidRegistrar, 'setEndpointsForDid') - setEndpointsForDidSpy.mockImplementationOnce(() => Promise.resolve(undefined)) - - const result = await indySdkSovDidRegistrar.create(agentContext, { - method: 'sov', - options: { - alias: 'Hello', - submitterVerificationMethod: 'did:sov:BzCbsNYhMrjHiqZDTUASHg#key-1', - role: 'STEWARD', - endpoints: { - endpoint: 'https://example.com/endpoint', - routingKeys: ['key-1'], - types: ['DIDComm', 'did-communication', 'endpoint'], - }, - }, - secret: { - privateKey, - }, - }) - - expect(registerPublicDidSpy).toHaveBeenCalledWith( - agentContext, - // Unqualified submitter did - 'BzCbsNYhMrjHiqZDTUASHg', - // submitter signing key, - expect.any(Key), - // Unqualified created indy did - 'R1xKJw17sUoXhejEpugMYJ', - // Verkey - expect.any(Key), - // Alias - 'Hello', - // Pool - { config: { indyNamespace: 'pool1' } }, - // Role - 'STEWARD' - ) - expect(JsonTransformer.toJSON(result)).toMatchObject({ - didDocumentMetadata: { - qualifiedIndyDid: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ', - }, - didRegistrationMetadata: { - didIndyNamespace: 'pool1', - }, - didState: { - state: 'finished', - did: 'did:sov:R1xKJw17sUoXhejEpugMYJ', - didDocument: { - '@context': [ - 'https://w3id.org/did/v1', - 'https://w3id.org/security/suites/ed25519-2018/v1', - 'https://w3id.org/security/suites/x25519-2019/v1', - 'https://didcomm.org/messaging/contexts/v2', - ], - id: 'did:sov:R1xKJw17sUoXhejEpugMYJ', - verificationMethod: [ - { - id: 'did:sov:R1xKJw17sUoXhejEpugMYJ#key-1', - type: 'Ed25519VerificationKey2018', - controller: 'did:sov:R1xKJw17sUoXhejEpugMYJ', - publicKeyBase58: 'E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu', - }, - { - id: 'did:sov:R1xKJw17sUoXhejEpugMYJ#key-agreement-1', - type: 'X25519KeyAgreementKey2019', - controller: 'did:sov:R1xKJw17sUoXhejEpugMYJ', - publicKeyBase58: 'Fbv17ZbnUSbafsiUBJbdGeC62M8v8GEscVMMcE59mRPt', - }, - ], - service: [ - { - id: 'did:sov:R1xKJw17sUoXhejEpugMYJ#endpoint', - serviceEndpoint: 'https://example.com/endpoint', - type: 'endpoint', - }, - { - id: 'did:sov:R1xKJw17sUoXhejEpugMYJ#did-communication', - serviceEndpoint: 'https://example.com/endpoint', - type: 'did-communication', - priority: 0, - recipientKeys: ['did:sov:R1xKJw17sUoXhejEpugMYJ#key-agreement-1'], - routingKeys: ['key-1'], - accept: ['didcomm/aip2;env=rfc19'], - }, - { - id: 'did:sov:R1xKJw17sUoXhejEpugMYJ#didcomm-1', - serviceEndpoint: 'https://example.com/endpoint', - type: 'DIDComm', - routingKeys: ['key-1'], - accept: ['didcomm/v2'], - }, - ], - authentication: ['did:sov:R1xKJw17sUoXhejEpugMYJ#key-1'], - assertionMethod: ['did:sov:R1xKJw17sUoXhejEpugMYJ#key-1'], - keyAgreement: ['did:sov:R1xKJw17sUoXhejEpugMYJ#key-agreement-1'], - }, - secret: { - privateKey, - }, - }, - }) - }) - - it('should store the did document', async () => { - const privateKey = TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c712fd969598e') - - const registerPublicDidSpy = jest.spyOn(indySdkSovDidRegistrar, 'registerPublicDid') - registerPublicDidSpy.mockImplementationOnce(() => Promise.resolve('did')) - - const setEndpointsForDidSpy = jest.spyOn(indySdkSovDidRegistrar, 'setEndpointsForDid') - setEndpointsForDidSpy.mockImplementationOnce(() => Promise.resolve(undefined)) - - const saveCalled = jest.fn() - eventEmitter.on>(RepositoryEventTypes.RecordSaved, saveCalled) - - await indySdkSovDidRegistrar.create(agentContext, { - method: 'sov', - options: { - alias: 'Hello', - submitterVerificationMethod: 'did:sov:BzCbsNYhMrjHiqZDTUASHg#key-1', - role: 'STEWARD', - endpoints: { - endpoint: 'https://example.com/endpoint', - routingKeys: ['key-1'], - types: ['DIDComm', 'did-communication', 'endpoint'], - }, - }, - secret: { - privateKey, - }, - }) - - expect(saveCalled).toHaveBeenCalledTimes(1) - const [saveEvent] = saveCalled.mock.calls[0] - - expect(saveEvent.payload.record).toMatchObject({ - did: 'did:sov:R1xKJw17sUoXhejEpugMYJ', - role: DidDocumentRole.Created, - _tags: { - recipientKeyFingerprints: ['z6LSrH6AdsQeZuKKmG6Ehx7abEQZsVg2psR2VU536gigUoAe'], - qualifiedIndyDid: 'did:indy:pool1:R1xKJw17sUoXhejEpugMYJ', - }, - didDocument: undefined, - }) - }) - - it('should return an error state when calling update', async () => { - const result = await indySdkSovDidRegistrar.update() - - expect(result).toEqual({ - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: `notImplemented: updating did:sov not implemented yet`, - }, - }) - }) - - it('should return an error state when calling deactivate', async () => { - const result = await indySdkSovDidRegistrar.deactivate() - - expect(result).toEqual({ - didDocumentMetadata: {}, - didRegistrationMetadata: {}, - didState: { - state: 'failed', - reason: `notImplemented: deactivating did:sov not implemented yet`, - }, - }) - }) -}) diff --git a/packages/indy-sdk/src/dids/__tests__/IndySdkSovDidResolver.test.ts b/packages/indy-sdk/src/dids/__tests__/IndySdkSovDidResolver.test.ts index c9bb1bbc93..6f4eabab97 100644 --- a/packages/indy-sdk/src/dids/__tests__/IndySdkSovDidResolver.test.ts +++ b/packages/indy-sdk/src/dids/__tests__/IndySdkSovDidResolver.test.ts @@ -23,6 +23,10 @@ mockFunction(indySdkPoolServiceMock.getPoolForNamespace).mockReturnValue({ config: { indyNamespace: 'pool1' }, } as IndySdkPool) +mockFunction(indySdkPoolServiceMock.getPoolForDid).mockResolvedValue({ + pool: { config: { indyNamespace: 'pool1' } } as IndySdkPool, +}) + const agentConfig = getAgentConfig('IndySdkSovDidResolver') const wallet = new IndySdkWallet(indySdk, agentConfig.logger, new SigningProviderRegistry([])) diff --git a/packages/indy-sdk/src/dids/__tests__/__fixtures__/didIndyPool1R1xKJw17sUoXhejEpugMYJ.json b/packages/indy-sdk/src/dids/__tests__/__fixtures__/didIndyPool1R1xKJw17sUoXhejEpugMYJ.json new file mode 100644 index 0000000000..c0bd51aa30 --- /dev/null +++ b/packages/indy-sdk/src/dids/__tests__/__fixtures__/didIndyPool1R1xKJw17sUoXhejEpugMYJ.json @@ -0,0 +1,50 @@ +{ + "@context": [ + "https://w3id.org/did/v1", + "https://w3id.org/security/suites/ed25519-2018/v1", + "https://w3id.org/security/suites/x25519-2019/v1" + ], + "id": "did:indy:pool1:R1xKJw17sUoXhejEpugMYJ", + "verificationMethod": [ + { + "type": "Ed25519VerificationKey2018", + "controller": "did:indy:pool1:R1xKJw17sUoXhejEpugMYJ", + "id": "did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#verkey", + "publicKeyBase58": "E6D1m3eERqCueX4ZgMCY14B4NceAr6XP2HyVqt55gDhu" + }, + { + "type": "X25519KeyAgreementKey2019", + "controller": "did:indy:pool1:R1xKJw17sUoXhejEpugMYJ", + "id": "did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#key-agreement-1", + "publicKeyBase58": "Fbv17ZbnUSbafsiUBJbdGeC62M8v8GEscVMMcE59mRPt" + } + ], + "authentication": ["did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#verkey"], + "keyAgreement": ["did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#key-agreement-1"], + "service": [ + { + "id": "did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#endpoint", + "type": "endpoint", + "serviceEndpoint": "https://ssi.com" + }, + { + "accept": ["didcomm/aip2;env=rfc19"], + "id": "did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#did-communication", + "priority": 0, + "recipientKeys": ["did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#key-agreement-1"], + "routingKeys": [], + "serviceEndpoint": "https://ssi.com", + "type": "did-communication" + }, + { + "id": "did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#profile", + "serviceEndpoint": "https://profile.com", + "type": "profile" + }, + { + "id": "did:indy:pool1:R1xKJw17sUoXhejEpugMYJ#hub", + "serviceEndpoint": "https://hub.com", + "type": "hub" + } + ] +} diff --git a/packages/indy-sdk/src/dids/__tests__/__fixtures__/didIndyPool1WJz9mHyW9BZksioQnRsrAo.json b/packages/indy-sdk/src/dids/__tests__/__fixtures__/didIndyPool1WJz9mHyW9BZksioQnRsrAo.json new file mode 100644 index 0000000000..a943f3bf9e --- /dev/null +++ b/packages/indy-sdk/src/dids/__tests__/__fixtures__/didIndyPool1WJz9mHyW9BZksioQnRsrAo.json @@ -0,0 +1,48 @@ +{ + "@context": [ + "https://w3id.org/did/v1", + "https://w3id.org/security/suites/ed25519-2018/v1", + "https://w3id.org/security/suites/x25519-2019/v1", + "https://didcomm.org/messaging/contexts/v2" + ], + "id": "did:indy:pool1:WJz9mHyW9BZksioQnRsrAo", + "verificationMethod": [ + { + "type": "Ed25519VerificationKey2018", + "controller": "did:indy:pool1:WJz9mHyW9BZksioQnRsrAo", + "id": "did:indy:pool1:WJz9mHyW9BZksioQnRsrAo#verkey", + "publicKeyBase58": "GyYtYWU1vjwd5PFJM4VSX5aUiSV3TyZMuLBJBTQvfdF8" + }, + { + "type": "X25519KeyAgreementKey2019", + "controller": "did:indy:pool1:WJz9mHyW9BZksioQnRsrAo", + "id": "did:indy:pool1:WJz9mHyW9BZksioQnRsrAo#key-agreement-1", + "publicKeyBase58": "S3AQEEKkGYrrszT9D55ozVVX2XixYp8uynqVm4okbud" + } + ], + "authentication": ["did:indy:pool1:WJz9mHyW9BZksioQnRsrAo#verkey"], + "keyAgreement": ["did:indy:pool1:WJz9mHyW9BZksioQnRsrAo#key-agreement-1"], + "service": [ + { + "id": "did:indy:pool1:WJz9mHyW9BZksioQnRsrAo#endpoint", + "type": "endpoint", + "serviceEndpoint": "https://agent.com" + }, + { + "id": "did:indy:pool1:WJz9mHyW9BZksioQnRsrAo#did-communication", + "type": "did-communication", + "priority": 0, + "recipientKeys": ["did:indy:pool1:WJz9mHyW9BZksioQnRsrAo#key-agreement-1"], + "routingKeys": ["routingKey1", "routingKey2"], + "accept": ["didcomm/aip2;env=rfc19"], + "serviceEndpoint": "https://agent.com" + }, + { + "id": "did:indy:pool1:WJz9mHyW9BZksioQnRsrAo#didcomm-1", + "type": "DIDComm", + "serviceEndpoint": "https://agent.com", + "accept": ["didcomm/v2"], + "routingKeys": ["routingKey1", "routingKey2"] + } + ] +} diff --git a/packages/indy-sdk/src/dids/didIndyUtil.ts b/packages/indy-sdk/src/dids/didIndyUtil.ts new file mode 100644 index 0000000000..af8adacf77 --- /dev/null +++ b/packages/indy-sdk/src/dids/didIndyUtil.ts @@ -0,0 +1,67 @@ +import type { AgentContext } from '@aries-framework/core' + +import { + getKeyFromVerificationMethod, + AriesFrameworkError, + convertPublicKeyToX25519, + DidDocumentBuilder, + DidsApi, + TypedArrayEncoder, +} from '@aries-framework/core' + +import { DID_INDY_REGEX } from '../utils/did' + +export function parseIndyDid(did: string) { + const match = did.match(DID_INDY_REGEX) + if (match) { + const [, namespace, id] = match + return { namespace, id } + } else { + throw new AriesFrameworkError(`${did} is not a valid did:indy did`) + } +} + +// Create a base DIDDoc template according to https://hyperledger.github.io/indy-did-method/#base-diddoc-template +export function indyDidDocumentFromDid(did: string, publicKeyBase58: string) { + const verificationMethodId = `${did}#verkey` + + const builder = new DidDocumentBuilder(did) + .addContext('https://w3id.org/security/suites/ed25519-2018/v1') + .addVerificationMethod({ + controller: did, + id: verificationMethodId, + publicKeyBase58, + type: 'Ed25519VerificationKey2018', + }) + .addAuthentication(verificationMethodId) + + return builder +} + +export function createKeyAgreementKey(verkey: string) { + return TypedArrayEncoder.toBase58(convertPublicKeyToX25519(TypedArrayEncoder.fromBase58(verkey))) +} + +/** + * Fetches the verification key for a given did:indy did and returns the key as a {@link Key} object. + * + * @throws {@link AriesFrameworkError} if the did could not be resolved or the key could not be extracted + */ +export async function verificationKeyForIndyDid(agentContext: AgentContext, did: string) { + // FIXME: we should store the didDocument in the DidRecord so we don't have to fetch our own did + // from the ledger to know which key is associated with the did + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const didResult = await didsApi.resolve(did) + + if (!didResult.didDocument) { + throw new AriesFrameworkError( + `Could not resolve did ${did}. ${didResult.didResolutionMetadata.error} ${didResult.didResolutionMetadata.message}` + ) + } + + // did:indy dids MUST have a verificationMethod with #verkey + const verificationMethod = didResult.didDocument.dereferenceKey(`${did}#verkey`) + const key = getKeyFromVerificationMethod(verificationMethod) + + return key +} diff --git a/packages/indy-sdk/src/dids/didSovUtil.ts b/packages/indy-sdk/src/dids/didSovUtil.ts index b5af6ee3f0..989e09f432 100644 --- a/packages/indy-sdk/src/dids/didSovUtil.ts +++ b/packages/indy-sdk/src/dids/didSovUtil.ts @@ -9,6 +9,8 @@ import { import { getFullVerkey } from '../utils/did' +export type DidCommServicesEndpointType = 'endpoint' | 'did-communication' | 'DIDComm' + export interface IndyEndpointAttrib { endpoint?: string types?: Array<'endpoint' | 'did-communication' | 'DIDComm'> @@ -16,6 +18,10 @@ export interface IndyEndpointAttrib { [key: string]: unknown } +/** + * Get a base did:sov did document based on the provided did and verkey + * https://sovrin-foundation.github.io/sovrin/spec/did-method-spec-template.html#crud-operation-definitions + */ export function sovDidDocumentFromDid(fullDid: string, verkey: string) { const verificationMethodId = `${fullDid}#key-1` const keyAgreementId = `${fullDid}#key-agreement-1` diff --git a/packages/indy-sdk/src/dids/index.ts b/packages/indy-sdk/src/dids/index.ts index 68eabe204d..8017bf8749 100644 --- a/packages/indy-sdk/src/dids/index.ts +++ b/packages/indy-sdk/src/dids/index.ts @@ -1,7 +1,3 @@ -export { - IndySdkSovDidRegistrar, - IndySdkSovDidCreateOptions, - IndySdkSovDidDeactivateOptions, - IndySdkSovDidUpdateOptions, -} from './IndySdkSovDidRegistrar' +export { IndySdkIndyDidRegistrar, IndySdkIndyDidCreateOptions } from './IndySdkIndyDidRegistrar' export { IndySdkSovDidResolver } from './IndySdkSovDidResolver' +export { IndySdkIndyDidResolver } from './IndySdkIndyDidResolver' diff --git a/packages/indy-sdk/src/index.ts b/packages/indy-sdk/src/index.ts index 64ed474b75..5857f00da2 100644 --- a/packages/indy-sdk/src/index.ts +++ b/packages/indy-sdk/src/index.ts @@ -1,11 +1,5 @@ // Dids -export { - IndySdkSovDidRegistrar, - IndySdkSovDidCreateOptions, - IndySdkSovDidDeactivateOptions, - IndySdkSovDidUpdateOptions, - IndySdkSovDidResolver, -} from './dids' +export * from './dids' // Ledger export { IndySdkPoolConfig } from './ledger' diff --git a/packages/indy-sdk/src/ledger/IndySdkPoolService.ts b/packages/indy-sdk/src/ledger/IndySdkPoolService.ts index be9217b0ed..23928fafa5 100644 --- a/packages/indy-sdk/src/ledger/IndySdkPoolService.ts +++ b/packages/indy-sdk/src/ledger/IndySdkPoolService.ts @@ -17,7 +17,7 @@ import { Subject } from 'rxjs' import { IndySdkModuleConfig } from '../IndySdkModuleConfig' import { IndySdkError, isIndyError } from '../error' import { assertIndySdkWallet } from '../utils/assertIndySdkWallet' -import { isSelfCertifiedDid } from '../utils/did' +import { DID_INDY_REGEX, isLegacySelfCertifiedDid } from '../utils/did' import { allSettled, onlyFulfilled, onlyRejected } from '../utils/promises' import { IndySdkPool } from './IndySdkPool' @@ -56,13 +56,39 @@ export class IndySdkPoolService { } /** - * Get the most appropriate pool for the given did. The algorithm is based on the approach as described in this document: + * Get the most appropriate pool for the given did. + * If the did is a qualified indy did, the pool will be determined based on the namespace. + * If it is a legacy unqualified indy did, the pool will be determined based on the algorithm as described in this document: * https://docs.google.com/document/d/109C_eMsuZnTnYe2OAd02jAts1vC4axwEKIq7_4dnNVA/edit + * + * This method will optionally return a nym response when the did has been resolved to determine the ledger + * either now or in the past. The nymResponse can be used to prevent multiple ledger quries fetching the same + * did */ public async getPoolForDid( agentContext: AgentContext, did: string - ): Promise<{ pool: IndySdkPool; did: GetNymResponse }> { + ): Promise<{ pool: IndySdkPool; nymResponse?: GetNymResponse }> { + // Check if the did starts with did:indy + const match = did.match(DID_INDY_REGEX) + + if (match) { + const [, namespace] = match + + const pool = this.getPoolForNamespace(namespace) + + if (pool) return { pool } + + throw new IndySdkPoolError(`Pool for indy namespace '${namespace}' not found`) + } else { + return await this.getPoolForLegacyDid(agentContext, did) + } + } + + private async getPoolForLegacyDid( + agentContext: AgentContext, + did: string + ): Promise<{ pool: IndySdkPool; nymResponse: GetNymResponse }> { const pools = this.pools if (pools.length === 0) { @@ -78,7 +104,7 @@ export class IndySdkPoolService { // If we have the nym response with associated pool in the cache, we'll use that if (cachedNymResponse && pool) { this.logger.trace(`Found ledger '${pool.didIndyNamespace}' for did '${did}' in cache`) - return { did: cachedNymResponse.nymResponse, pool } + return { nymResponse: cachedNymResponse.nymResponse, pool } } const { successful, rejected } = await this.getSettledDidResponsesFromPools(did, pools) @@ -103,7 +129,7 @@ export class IndySdkPoolService { // We take the first self certifying DID as we take the order in the // indyLedgers config as the order of preference of ledgers let value = successful.find((response) => - isSelfCertifiedDid(response.value.did.did, response.value.did.verkey) + isLegacySelfCertifiedDid(response.value.did.did, response.value.did.verkey) )?.value if (!value) { @@ -123,7 +149,8 @@ export class IndySdkPoolService { nymResponse: value.did, indyNamespace: value.pool.didIndyNamespace, } satisfies CachedDidResponse) - return { pool: value.pool, did: value.did } + + return { pool: value.pool, nymResponse: value.did } } private async getSettledDidResponsesFromPools(did: string, pools: IndySdkPool[]) { diff --git a/packages/indy-sdk/src/ledger/__tests__/IndySdkPoolService.test.ts b/packages/indy-sdk/src/ledger/__tests__/IndySdkPoolService.test.ts index ee595c31ec..20d79e0564 100644 --- a/packages/indy-sdk/src/ledger/__tests__/IndySdkPoolService.test.ts +++ b/packages/indy-sdk/src/ledger/__tests__/IndySdkPoolService.test.ts @@ -115,6 +115,12 @@ describe('IndySdkPoolService', () => { expect(poolService.getPoolForDid(agentContext, did)).rejects.toThrowError(IndySdkPoolNotFoundError) }) + it('should return the pool based on namespace if did is a valid did:indy did', async () => { + const { pool } = await poolService.getPoolForDid(agentContext, 'did:indy:sovrin:Y5bj4SjCiTM9PgeheKAiXx') + + expect(pool.didIndyNamespace).toBe('sovrin') + }) + it('should return the pool if the did was only found on one ledger', async () => { const did = 'TL1EaPFCZ8Si5aUrqScBDt' // Only found on one ledger diff --git a/packages/indy-sdk/src/utils/__tests__/did.test.ts b/packages/indy-sdk/src/utils/__tests__/did.test.ts index 45344136d9..222f9898fd 100644 --- a/packages/indy-sdk/src/utils/__tests__/did.test.ts +++ b/packages/indy-sdk/src/utils/__tests__/did.test.ts @@ -1,4 +1,4 @@ -import { isAbbreviatedVerkey, isFullVerkey, isSelfCertifiedDid } from '../did' +import { isAbbreviatedVerkey, isFullVerkey, isLegacySelfCertifiedDid } from '../did' const validAbbreviatedVerkeys = [ '~PKAYz8Ev4yoQgr2LaMAWFx', @@ -36,15 +36,19 @@ const invalidFullVerkeys = [ describe('Utils | Did', () => { describe('isSelfCertifiedDid()', () => { test('returns true if the verkey is abbreviated', () => { - expect(isSelfCertifiedDid('PW8ZHpNupeWXbmpPWog6Ki', '~QQ5jiH1dgXPAnvHdJvazn9')).toBe(true) + expect(isLegacySelfCertifiedDid('PW8ZHpNupeWXbmpPWog6Ki', '~QQ5jiH1dgXPAnvHdJvazn9')).toBe(true) }) test('returns true if the verkey is not abbreviated and the did is generated from the verkey', () => { - expect(isSelfCertifiedDid('Y8q4Aq6gRAcmB6jjKk3Z7t', 'HyEoPRNvC7q4jj5joUo8AWYtxbNccbEnTAeuMYkpmNS2')).toBe(true) + expect(isLegacySelfCertifiedDid('Y8q4Aq6gRAcmB6jjKk3Z7t', 'HyEoPRNvC7q4jj5joUo8AWYtxbNccbEnTAeuMYkpmNS2')).toBe( + true + ) }) test('returns false if the verkey is not abbreviated and the did is not generated from the verkey', () => { - expect(isSelfCertifiedDid('Y8q4Aq6gRAcmB6jjKk3Z7t', 'AcU7DnRqoXGYATD6VqsRq4eHuz55gdM3uzFBEhFd6rGh')).toBe(false) + expect(isLegacySelfCertifiedDid('Y8q4Aq6gRAcmB6jjKk3Z7t', 'AcU7DnRqoXGYATD6VqsRq4eHuz55gdM3uzFBEhFd6rGh')).toBe( + false + ) }) }) diff --git a/packages/indy-sdk/src/utils/did.ts b/packages/indy-sdk/src/utils/did.ts index 90b465a0f6..7d78cd09e2 100644 --- a/packages/indy-sdk/src/utils/did.ts +++ b/packages/indy-sdk/src/utils/did.ts @@ -19,6 +19,7 @@ import { Buffer, TypedArrayEncoder } from '@aries-framework/core' export const FULL_VERKEY_REGEX = /^[1-9A-HJ-NP-Za-km-z]{43,44}$/ export const ABBREVIATED_VERKEY_REGEX = /^~[1-9A-HJ-NP-Za-km-z]{21,22}$/ +export const DID_INDY_REGEX = /^did:indy:((?:[a-z][_a-z0-9-]*)(?::[a-z][_a-z0-9-]*)?):([1-9A-HJ-NP-Za-km-z]{21,22})$/ /** * Check whether the did is a self certifying did. If the verkey is abbreviated this method @@ -27,14 +28,14 @@ export const ABBREVIATED_VERKEY_REGEX = /^~[1-9A-HJ-NP-Za-km-z]{21,22}$/ * * @return Boolean indicating whether the did is self certifying */ -export function isSelfCertifiedDid(did: string, verkey: string): boolean { +export function isLegacySelfCertifiedDid(did: string, verkey: string): boolean { // If the verkey is Abbreviated, it means the full verkey // is the did + the verkey if (isAbbreviatedVerkey(verkey)) { return true } - const didFromVerkey = indyDidFromPublicKeyBase58(verkey) + const didFromVerkey = legacyIndyDidFromPublicKeyBase58(verkey) if (didFromVerkey === did) { return true @@ -43,7 +44,7 @@ export function isSelfCertifiedDid(did: string, verkey: string): boolean { return false } -export function indyDidFromPublicKeyBase58(publicKeyBase58: string): string { +export function legacyIndyDidFromPublicKeyBase58(publicKeyBase58: string): string { const buffer = TypedArrayEncoder.fromBase58(publicKeyBase58) const did = TypedArrayEncoder.toBase58(buffer.slice(0, 16)) diff --git a/packages/indy-sdk/tests/__fixtures__/anoncreds.ts b/packages/indy-sdk/tests/__fixtures__/anoncreds.ts new file mode 100644 index 0000000000..eb978ec748 --- /dev/null +++ b/packages/indy-sdk/tests/__fixtures__/anoncreds.ts @@ -0,0 +1,30 @@ +export const credentialDefinitionValue = { + primary: { + n: '96517142458750088826087901549537285521906361834839650465292394026155791790248920518228426560592477800345470631128393537910767968076647428853737338120375137978526133371095345886547568849980095910835456337942570110635942227498396677781945046904040000347997661394155645138402989185582727368743644878567330299129483548946710969360956979880962101169330048328620192831242584775824654760726417810662811409929761424969870024291961980782988854217354212087291593903213167261779548063894662259300608395552269380441482047725811646638173390809967510159302372018819245039226007682154490256871635806558216146474297742733244470144481', + s: '20992997088800769394205042281221010730843336204635587269131066142238627416871294692123680065003125450990475247419429111144686875080339959479648984195457400282722471552678361441816569115316390063503704185107464429408708889920969284364549487320740759452356010336698287092961864738455949515401889999320804333605635972368885179914619910494573144273759358510644118555354521660927445864167887629319425342133470781407706668100509422240127902573158722086763638357241708157836231326104213948080124231104027985997092193458353052131052627451830345602820935886233072722689872803371231173593216542422645374438328309647440653637339', + r: { + master_secret: + '96243300745227716230048295249700256382424379142767068560156597061550615821183969840133023439359733351013932957841392861447122785423145599004240865527901625751619237368187131360686977600247815596986496835118582544022443932674638843143227258367859921648385998241629365673854479167826898057354386557912400420925145402535066400276579674049751639901555837852972622061540154688641944145082381483273814616102862399655638465723909813901943343059991047747289931252070264205125933226649905593045675877143065756794349492159868513288280364195700788501708587588090219665708038121636837649207584981238653023213330207384929738192210', + name: '73301750658973501389860306433954162777688414647250690792688553201037736559940890441467927863421690990807820789906540409252803697381653459639864945429958798104818241892796218340966964349674689564019059435289373607451125919476002261041343187491848656595845611576458601110066647002078334660251906541846222115184239401618625285703919125402959929850028352261117167621349930047514115676870868726855651130262227714591240534532398809967792128535084773798290351459391475237061458901325844643172504167457543287673202618731404966555015061917662865397763636445953946274068384614117513804834235388565249331682010365807270858083546', + }, + rctxt: + '37788128721284563440858950515231840450431543928224096081933216180465915572829884228780081835462293611329848268384962871736884632087015070623933628853658097637604059748079512999518737243304794110313829761155878287344472916564970806851294430356498883927870926898737394894892797927804721407643833828162246495645836390303263072281761384240973982733122383052566872688887552226083782030670443318152427129452272570595367287061688769394567289624972332234661767648489253220495098949161964171486245324730862072203259801377135500275012560207100571502032523912388082460843991502336467718632746396226650194750972544436894286230063', + z: '43785356695890052462955676926428400928903479009358861113206349419200366390858322895540291303484939601128045362682307382393826375825484851021601464391509750565285197155653613669680662395620338416776539485377195826876505126073018100680273457526216247879013350460071029101583221000647494610122617904515744711339846577920055655093367012508192004131719432915903924789974568341538556528133188398290594619318653419602058489178526243446782729272985727332736198326183868783570550373552407121582843992983431205917273352230155794805507408743590383242904107596623095433284330566906935063373759426916339149701872288610119965287995', + }, + revocation: { + g: '1 0A84C28144BC8B677839038FFFA824AB5ADE517F8DD4A89F092FAF9A3560C62D 1 00FD708E112EEA5D89AF9D0559795E6DBCF56D3B8CDF79EFF34A72EB741F896F 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8', + g_dash: + '1 201F3E23CC7E9284F3EFCF9500F1E2537C398EAB2E94D2EB801AECC7FBFBDC01 1 08132C7723CF9861D4CC24B56555EF1CBD9AE746C97B3ADFA36C669F2DCE09B6 1 1B2397FB2A1ADE704E2A1E4C242612F4677F9F1BD09E6B14C2E77E25EDA4C62E 1 00CDC2CF5F278D699D52223577AB032C150A3CB4C8E8AB07AB9D592772910E95 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000', + h: '1 072E0A505004F2F32B4210E72FA18A2ADF17F31479BD2059B7A8C0BA58F2ACB3 1 05C70F039E60317003C41C319753ECACC629791FDB06D6ADC5B06DD94501B973 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8', + h0: '1 03CBE26D18118E9770D4A0B3E8607B3B3A8D3D3CA81FF8D41862430CC583156E 1 004A2A57E0A826AEFF007EDDAF89B02F054050843689167B10127FE9EDEEEDA9 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8', + h1: '1 10C9F9DE537994E4FEF2625AFA78342C8A096238A875F6899DD500230E6022E5 1 0C0A88F53D020557377B4ED9C3826E9B8F918DD03E23B0F8ECD922F8333359D3 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8', + h2: '1 017F748AEEC1DDE4E4C3FBAE771C041F0A6FAEAF34FD02AF773AC4B75025147B 1 1298DBD9A4BEE6AD54E060A57BCE932735B7738C30A9ADAEFE2F38E1858A0183 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8', + htilde: + '1 0C471F0451D6AC352E28B6ECDE8D7233B75530AE59276DF0F4B9A8B0C5C7E5DB 1 24CE4461910AA5D60C09C24EE0FE51E1B1600D8BA6E483E9050EF897CA3E3C8A 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8', + h_cap: + '1 225B2106DEBD353AABDFC4C7F7E8660D308FB514EA9DAE0533DDEB65CF796159 1 1F6093622F439FC22C64F157F4F35F7C592EC0169C6F0026BC44CD3E375974A7 1 142126FAC3657AD846D394E1F72FD01ECC15E84416713CD133980E324B24F4BC 1 0357995DBDCD4385E59E607761AB30AE8D9DDE005A777EE846EF51AE2816CD33 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000', + u: '1 00D8DDC2EB6536CA320EE035D099937E59B11678162C1BFEB30C58FCA9F84650 1 1557A5B05A1A30D63322E187D323C9CA431BC5E811E68D4703933D9DDA26D299 1 10E8AB93AA87839B757521742EBA23C3B257C91F61A93D37AEC4C0A011B5F073 1 1DA65E40406A7875DA8CFCE9FD7F283145C166382A937B72819BDC335FE9A734 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000', + pk: '1 1A7EBBE3E7F8ED50959851364B20997944FA8AE5E3FC0A2BB531BAA17179D320 1 02C55FE6F64A2A4FF49B37C513C39E56ECD565CFAD6CA46DC6D8095179351863 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8', + y: '1 1BF97F07270EC21A89E43BCA645D86A755F846B547238F1DA379E088CDD9B40D 1 146BB00F56FFC0DEF6541CEB484C718559B398DB1547B52850E46B23144161F1 1 079A1BEF8DFFA4E6352F701D476664340E7FBE5D3F46B897412BD2B5F10E33D7 1 02FDC508AEF90FB11961AF332BE4037973C76B954FFA48848F7E0588E93FCA8C 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000', + }, +} diff --git a/packages/indy-sdk/tests/sov-did-registrar.e2e.test.ts b/packages/indy-sdk/tests/indy-did-registrar.e2e.test.ts similarity index 68% rename from packages/indy-sdk/tests/sov-did-registrar.e2e.test.ts rename to packages/indy-sdk/tests/indy-did-registrar.e2e.test.ts index 4518010326..c007eaa561 100644 --- a/packages/indy-sdk/tests/sov-did-registrar.e2e.test.ts +++ b/packages/indy-sdk/tests/indy-did-registrar.e2e.test.ts @@ -1,14 +1,14 @@ -import type { IndySdkSovDidCreateOptions } from '../src/dids/IndySdkSovDidRegistrar' +import type { IndySdkIndyDidCreateOptions } from '../src' import { Agent, TypedArrayEncoder, convertPublicKeyToX25519, JsonTransformer } from '@aries-framework/core' import { generateKeyPairFromSeed } from '@stablelib/ed25519' import { getAgentOptions, importExistingIndyDidFromPrivateKey, publicDidSeed } from '../../core/tests' -import { indyDidFromPublicKeyBase58 } from '../src/utils/did' +import { legacyIndyDidFromPublicKeyBase58 } from '../src/utils/did' import { getIndySdkModules } from './setupIndySdkModule' -const agentOptions = getAgentOptions('Faber Dids Registrar', {}, getIndySdkModules()) +const agentOptions = getAgentOptions('Indy Sdk Indy Did Registrar', {}, getIndySdkModules()) describe('dids', () => { let agent: Agent> @@ -23,7 +23,7 @@ describe('dids', () => { await agent.wallet.delete() }) - it('should create a did:sov did', async () => { + it('should create a did:indy did', async () => { // Add existing endorser did to the wallet const unqualifiedSubmitterDid = await importExistingIndyDidFromPrivateKey( agent, @@ -41,12 +41,12 @@ describe('dids', () => { const publicKeyEd25519 = generateKeyPairFromSeed(privateKey).publicKey const x25519PublicKeyBase58 = TypedArrayEncoder.toBase58(convertPublicKeyToX25519(publicKeyEd25519)) const ed25519PublicKeyBase58 = TypedArrayEncoder.toBase58(publicKeyEd25519) - const indyDid = indyDidFromPublicKeyBase58(ed25519PublicKeyBase58) + const unqualifiedDid = legacyIndyDidFromPublicKeyBase58(ed25519PublicKeyBase58) - const did = await agent.dids.create({ - method: 'sov', + const did = await agent.dids.create({ + method: 'indy', options: { - submitterVerificationMethod: `did:sov:${unqualifiedSubmitterDid}#key-1`, + submitterDid: `did:indy:pool:localtest:${unqualifiedSubmitterDid}`, alias: 'Alias', endpoints: { endpoint: 'https://example.com/endpoint', @@ -60,15 +60,11 @@ describe('dids', () => { }) expect(JsonTransformer.toJSON(did)).toMatchObject({ - didDocumentMetadata: { - qualifiedIndyDid: `did:indy:pool:localtest:${indyDid}`, - }, - didRegistrationMetadata: { - didIndyNamespace: 'pool:localtest', - }, + didDocumentMetadata: {}, + didRegistrationMetadata: {}, didState: { state: 'finished', - did: `did:sov:${indyDid}`, + did: `did:indy:pool:localtest:${unqualifiedDid}`, didDocument: { '@context': [ 'https://w3id.org/did/v1', @@ -80,47 +76,47 @@ describe('dids', () => { controller: undefined, verificationMethod: [ { - id: `did:sov:${indyDid}#key-1`, + id: `did:indy:pool:localtest:${unqualifiedDid}#verkey`, type: 'Ed25519VerificationKey2018', - controller: `did:sov:${indyDid}`, + controller: `did:indy:pool:localtest:${unqualifiedDid}`, publicKeyBase58: ed25519PublicKeyBase58, }, { - id: `did:sov:${indyDid}#key-agreement-1`, + id: `did:indy:pool:localtest:${unqualifiedDid}#key-agreement-1`, type: 'X25519KeyAgreementKey2019', - controller: `did:sov:${indyDid}`, + controller: `did:indy:pool:localtest:${unqualifiedDid}`, publicKeyBase58: x25519PublicKeyBase58, }, ], service: [ { - id: `did:sov:${indyDid}#endpoint`, + id: `did:indy:pool:localtest:${unqualifiedDid}#endpoint`, serviceEndpoint: 'https://example.com/endpoint', type: 'endpoint', }, { accept: ['didcomm/aip2;env=rfc19'], - id: `did:sov:${indyDid}#did-communication`, + id: `did:indy:pool:localtest:${unqualifiedDid}#did-communication`, priority: 0, - recipientKeys: [`did:sov:${indyDid}#key-agreement-1`], + recipientKeys: [`did:indy:pool:localtest:${unqualifiedDid}#key-agreement-1`], routingKeys: ['a-routing-key'], serviceEndpoint: 'https://example.com/endpoint', type: 'did-communication', }, { accept: ['didcomm/v2'], - id: `did:sov:${indyDid}#didcomm-1`, + id: `did:indy:pool:localtest:${unqualifiedDid}#didcomm-1`, routingKeys: ['a-routing-key'], serviceEndpoint: 'https://example.com/endpoint', type: 'DIDComm', }, ], - authentication: [`did:sov:${indyDid}#key-1`], - assertionMethod: [`did:sov:${indyDid}#key-1`], - keyAgreement: [`did:sov:${indyDid}#key-agreement-1`], + authentication: [`did:indy:pool:localtest:${unqualifiedDid}#verkey`], + assertionMethod: undefined, + keyAgreement: [`did:indy:pool:localtest:${unqualifiedDid}#key-agreement-1`], capabilityInvocation: undefined, capabilityDelegation: undefined, - id: `did:sov:${indyDid}`, + id: `did:indy:pool:localtest:${unqualifiedDid}`, }, secret: { privateKey, diff --git a/packages/indy-sdk/tests/indy-did-resolver.e2e.test.ts b/packages/indy-sdk/tests/indy-did-resolver.e2e.test.ts new file mode 100644 index 0000000000..839db5e4df --- /dev/null +++ b/packages/indy-sdk/tests/indy-did-resolver.e2e.test.ts @@ -0,0 +1,99 @@ +import type { IndySdkIndyDidCreateOptions } from '../src' + +import { Agent, AriesFrameworkError, JsonTransformer, TypedArrayEncoder } from '@aries-framework/core' + +import { getAgentOptions, importExistingIndyDidFromPrivateKey, publicDidSeed } from '../../core/tests/helpers' + +import { getIndySdkModules } from './setupIndySdkModule' + +const agent = new Agent(getAgentOptions('Indy SDK Indy DID resolver', {}, getIndySdkModules())) + +describe('Indy SDK Indy DID resolver', () => { + beforeAll(async () => { + await agent.initialize() + }) + + afterAll(async () => { + await agent.shutdown() + await agent.wallet.delete() + }) + + it('should resolve a did:indy did', async () => { + // Add existing endorser did to the wallet + const unqualifiedSubmitterDid = await importExistingIndyDidFromPrivateKey( + agent, + TypedArrayEncoder.fromString(publicDidSeed) + ) + + const createResult = await agent.dids.create({ + method: 'indy', + options: { + submitterDid: `did:indy:pool:localtest:${unqualifiedSubmitterDid}`, + alias: 'Alias', + role: 'TRUSTEE', + endpoints: { + endpoint: 'http://localhost:3000', + }, + }, + }) + + // Terrible, but the did can't be immediately resolved, so we need to wait a bit + await new Promise((res) => setTimeout(res, 1000)) + + if (!createResult.didState.did) throw new AriesFrameworkError('Unable to register did') + + const didResult = await agent.dids.resolve(createResult.didState.did) + + expect(JsonTransformer.toJSON(didResult)).toMatchObject({ + didDocument: { + '@context': [ + 'https://w3id.org/did/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + ], + id: createResult.didState.did, + alsoKnownAs: undefined, + controller: undefined, + verificationMethod: [ + { + type: 'Ed25519VerificationKey2018', + controller: createResult.didState.did, + id: `${createResult.didState.did}#verkey`, + publicKeyBase58: expect.any(String), + }, + { + controller: createResult.didState.did, + type: 'X25519KeyAgreementKey2019', + id: `${createResult.didState.did}#key-agreement-1`, + publicKeyBase58: expect.any(String), + }, + ], + capabilityDelegation: undefined, + capabilityInvocation: undefined, + authentication: [`${createResult.didState.did}#verkey`], + assertionMethod: undefined, + keyAgreement: [`${createResult.didState.did}#key-agreement-1`], + service: [ + { + id: `${createResult.didState.did}#endpoint`, + serviceEndpoint: 'http://localhost:3000', + type: 'endpoint', + }, + { + id: `${createResult.didState.did}#did-communication`, + accept: ['didcomm/aip2;env=rfc19'], + priority: 0, + recipientKeys: [`${createResult.didState.did}#key-agreement-1`], + routingKeys: [], + serviceEndpoint: 'http://localhost:3000', + type: 'did-communication', + }, + ], + }, + didDocumentMetadata: {}, + didResolutionMetadata: { + contentType: 'application/did+ld+json', + }, + }) + }) +}) diff --git a/packages/indy-sdk/tests/indy-sdk-anoncreds-registry.e2e.test.ts b/packages/indy-sdk/tests/indy-sdk-anoncreds-registry.e2e.test.ts index 046767b8cc..77e034941d 100644 --- a/packages/indy-sdk/tests/indy-sdk-anoncreds-registry.e2e.test.ts +++ b/packages/indy-sdk/tests/indy-sdk-anoncreds-registry.e2e.test.ts @@ -1,4 +1,4 @@ -import { Agent, TypedArrayEncoder } from '@aries-framework/core' +import { Agent, Key, KeyType, TypedArrayEncoder } from '@aries-framework/core' import { agentDependencies, @@ -7,8 +7,11 @@ import { publicDidSeed, } from '../../core/tests/helpers' import { IndySdkAnonCredsRegistry } from '../src/anoncreds/services/IndySdkAnonCredsRegistry' +import { IndySdkPoolService } from '../src/ledger' +import { assertIndySdkWallet } from '../src/utils/assertIndySdkWallet' -import { getIndySdkModules } from './setupIndySdkModule' +import { credentialDefinitionValue } from './__fixtures__/anoncreds' +import { getIndySdkModules, indySdk } from './setupIndySdkModule' const agentConfig = getAgentConfig('IndySdkAnonCredsRegistry') const indySdkModules = getIndySdkModules() @@ -20,6 +23,8 @@ const agent = new Agent({ }) const indySdkAnonCredsRegistry = new IndySdkAnonCredsRegistry() +const indySdkPoolService = agent.dependencyManager.resolve(IndySdkPoolService) +const pool = indySdkPoolService.getPoolForNamespace('pool:localtest') describe('IndySdkAnonCredsRegistry', () => { beforeAll(async () => { @@ -34,20 +39,26 @@ describe('IndySdkAnonCredsRegistry', () => { await agent.wallet.delete() }) + // TODO: use different issuer for schema and credential definition to catch possible bugs // One test as the credential definition depends on the schema test('register and resolve a schema and credential definition', async () => { const dynamicVersion = `1.${Math.random() * 100}` + const legacyIssuerId = 'TL1EaPFCZ8Si5aUrqScBDt' + const signingKey = Key.fromPublicKeyBase58('FMGcFuU3QwAQLywxvmEnSorQT3NwU9wgDMMTaDFtvswm', KeyType.Ed25519) + const didIndyIssuerId = 'did:indy:pool:localtest:TL1EaPFCZ8Si5aUrqScBDt' + + const legacySchemaId = `TL1EaPFCZ8Si5aUrqScBDt:2:test:${dynamicVersion}` + const didIndySchemaId = `did:indy:pool:localtest:TL1EaPFCZ8Si5aUrqScBDt/anoncreds/v0/SCHEMA/test/${dynamicVersion}` + const schemaResult = await indySdkAnonCredsRegistry.registerSchema(agent.context, { schema: { attrNames: ['name'], - issuerId: 'TL1EaPFCZ8Si5aUrqScBDt', - name: 'test - 11', + issuerId: didIndyIssuerId, + name: 'test', version: dynamicVersion, }, - options: { - didIndyNamespace: 'pool:localtest', - }, + options: {}, }) expect(schemaResult).toMatchObject({ @@ -55,35 +66,31 @@ describe('IndySdkAnonCredsRegistry', () => { state: 'finished', schema: { attrNames: ['name'], - issuerId: 'TL1EaPFCZ8Si5aUrqScBDt', - name: 'test - 11', + issuerId: didIndyIssuerId, + name: 'test', version: dynamicVersion, }, - schemaId: `TL1EaPFCZ8Si5aUrqScBDt:2:test - 11:${dynamicVersion}`, + schemaId: didIndySchemaId, }, registrationMetadata: {}, schemaMetadata: { indyLedgerSeqNo: expect.any(Number), - didIndyNamespace: 'pool:localtest', }, }) // Wait some time before resolving credential definition object await new Promise((res) => setTimeout(res, 1000)) - const schemaResponse = await indySdkAnonCredsRegistry.getSchema( - agent.context, - `TL1EaPFCZ8Si5aUrqScBDt:2:test - 11:${dynamicVersion}` - ) - - expect(schemaResponse).toMatchObject({ + // Resolve using legacy schema id + const legacySchema = await indySdkAnonCredsRegistry.getSchema(agent.context, legacySchemaId) + expect(legacySchema).toMatchObject({ schema: { attrNames: ['name'], - name: 'test - 11', + name: 'test', version: dynamicVersion, issuerId: 'TL1EaPFCZ8Si5aUrqScBDt', }, - schemaId: `TL1EaPFCZ8Si5aUrqScBDt:2:test - 11:${dynamicVersion}`, + schemaId: `TL1EaPFCZ8Si5aUrqScBDt:2:test:${dynamicVersion}`, resolutionMetadata: {}, schemaMetadata: { didIndyNamespace: 'pool:localtest', @@ -91,58 +98,47 @@ describe('IndySdkAnonCredsRegistry', () => { }, }) + // Resolve using did indy schema id + const didIndySchema = await indySdkAnonCredsRegistry.getSchema(agent.context, didIndySchemaId) + expect(didIndySchema).toMatchObject({ + schema: { + attrNames: ['name'], + name: 'test', + version: dynamicVersion, + issuerId: didIndyIssuerId, + }, + schemaId: didIndySchemaId, + resolutionMetadata: {}, + schemaMetadata: { + didIndyNamespace: 'pool:localtest', + indyLedgerSeqNo: expect.any(Number), + }, + }) + + const legacyCredentialDefinitionId = `TL1EaPFCZ8Si5aUrqScBDt:3:CL:${schemaResult.schemaMetadata.indyLedgerSeqNo}:TAG` + const didIndyCredentialDefinitionId = `did:indy:pool:localtest:TL1EaPFCZ8Si5aUrqScBDt/anoncreds/v0/CLAIM_DEF/${schemaResult.schemaMetadata.indyLedgerSeqNo}/TAG` const credentialDefinitionResult = await indySdkAnonCredsRegistry.registerCredentialDefinition(agent.context, { credentialDefinition: { - issuerId: 'TL1EaPFCZ8Si5aUrqScBDt', + issuerId: didIndyIssuerId, tag: 'TAG', - schemaId: `TL1EaPFCZ8Si5aUrqScBDt:2:test - 11:${dynamicVersion}`, + schemaId: didIndySchemaId, type: 'CL', - value: { - primary: { - n: '92511867718854414868106363741369833735017762038454769060600859608405811709675033445666654908195955460485998711087020152978597220168927505650092431295783175164390266561239892662085428655566792056852960599485298025843840058914610127716620252006466964070280255168745873592143068949458568751438337748294055976926080232538440619420568859737673474560851456027625679328271511966332808025880807996449998057729417608399774744254122385012832309402226532031122728445959276178939234308090390331654445053482963947804769291501664200141562885660084823885847247231002821472258218384342423605116504024514572826071246440130942849549441', - s: '80388543865249952799447792504739237616187770512259677275061283897050980768551818104137338144380636412773836688624071360386172349725818126495487584981520630638409717065318132420766896092370913800616033623618952639023946750307405126873476182540669638841562357523429245685476919178722373320218824590869735129801004394337640642997250464303104754942997839179333543643110326022824394934965538190976474473353762308333205671176627192797138375084260446324344637548455228161138089974447059481109651156379803576163576511072261388342837813901850712083922506433336723723235701670225584863772222447543742649328218950436824219992164', - r: { - age: '676933340341980399002624386891134393471002096508227567343731826159610079436978196421307099268754545293545727546242372579987825752872485684085629459107300175443328323289748793060894500514926703654606851666031895448970879827423190730510730624784665299646624113512701254199984520803796529034094958026048762178753193812250643294518237843809104055653333871102658177900702978008644780459400512716361564897282969982554031820285585105004870317861287847206222714589633178648982299799311192432563797220854755882933052881306804544233529886513105815543097685128456041780804442879272476590077760678785460726492895806240870944398', - master_secret: - '57770757113548032970308439965749734133430520933173186296299026579579930337912607419798836831937319372744879560676750427054135869214212225572618340088847222727882935159356459822445182287686057012197046378986248048722180093079919306125315662058290895629438767985427829790980355162853804522854494960613869765167538645624719923127052541372069255024631093663068055100579264049925388231368871107383977060590248865498902704546409806115171120555709438784189721957301548212242748685629860268468247494986146122636455769804467583612610341632602695197189514316033637331733820369170763954604394734655429769801516997967996980978751', - }, - rctxt: - '19574881057684356733946284215946569464410211018678168661028327420122678446653210056362495902735819742274128834330867933095119512313591151219353395069123546495720010325822330866859140765940839241212947354612836044244554152389691282543839111284006009168728161183863936810142428875817934316327118674532328892591410224676539770085459540786747902789677759379901079898127879301595929571621032704093287675668250862222728331030586585586110859977896767318814398026750215625180255041545607499673023585546720788973882263863911222208020438685873501025545464213035270207099419236974668665979962146355749687924650853489277747454993', - z: '18569464356833363098514177097771727133940629758890641648661259687745137028161881113251218061243607037717553708179509640909238773964066423807945164288256211132195919975343578956381001087353353060599758005375631247614777454313440511375923345538396573548499287265163879524050255226779884271432737062283353279122281220812931572456820130441114446870167673796490210349453498315913599982158253821945225264065364670730546176140788405935081171854642125236557475395879246419105888077042924382595999612137336915304205628167917473420377397118829734604949103124514367857266518654728464539418834291071874052392799652266418817991437', - }, - }, - }, - options: { - didIndyNamespace: 'pool:localtest', + value: credentialDefinitionValue, }, + options: {}, }) expect(credentialDefinitionResult).toMatchObject({ - credentialDefinitionMetadata: { - didIndyNamespace: 'pool:localtest', - }, + credentialDefinitionMetadata: {}, credentialDefinitionState: { credentialDefinition: { - issuerId: 'TL1EaPFCZ8Si5aUrqScBDt', + issuerId: didIndyIssuerId, tag: 'TAG', - schemaId: `TL1EaPFCZ8Si5aUrqScBDt:2:test - 11:${dynamicVersion}`, + schemaId: didIndySchemaId, type: 'CL', - value: { - primary: { - n: '92511867718854414868106363741369833735017762038454769060600859608405811709675033445666654908195955460485998711087020152978597220168927505650092431295783175164390266561239892662085428655566792056852960599485298025843840058914610127716620252006466964070280255168745873592143068949458568751438337748294055976926080232538440619420568859737673474560851456027625679328271511966332808025880807996449998057729417608399774744254122385012832309402226532031122728445959276178939234308090390331654445053482963947804769291501664200141562885660084823885847247231002821472258218384342423605116504024514572826071246440130942849549441', - s: '80388543865249952799447792504739237616187770512259677275061283897050980768551818104137338144380636412773836688624071360386172349725818126495487584981520630638409717065318132420766896092370913800616033623618952639023946750307405126873476182540669638841562357523429245685476919178722373320218824590869735129801004394337640642997250464303104754942997839179333543643110326022824394934965538190976474473353762308333205671176627192797138375084260446324344637548455228161138089974447059481109651156379803576163576511072261388342837813901850712083922506433336723723235701670225584863772222447543742649328218950436824219992164', - r: { - age: '676933340341980399002624386891134393471002096508227567343731826159610079436978196421307099268754545293545727546242372579987825752872485684085629459107300175443328323289748793060894500514926703654606851666031895448970879827423190730510730624784665299646624113512701254199984520803796529034094958026048762178753193812250643294518237843809104055653333871102658177900702978008644780459400512716361564897282969982554031820285585105004870317861287847206222714589633178648982299799311192432563797220854755882933052881306804544233529886513105815543097685128456041780804442879272476590077760678785460726492895806240870944398', - master_secret: - '57770757113548032970308439965749734133430520933173186296299026579579930337912607419798836831937319372744879560676750427054135869214212225572618340088847222727882935159356459822445182287686057012197046378986248048722180093079919306125315662058290895629438767985427829790980355162853804522854494960613869765167538645624719923127052541372069255024631093663068055100579264049925388231368871107383977060590248865498902704546409806115171120555709438784189721957301548212242748685629860268468247494986146122636455769804467583612610341632602695197189514316033637331733820369170763954604394734655429769801516997967996980978751', - }, - rctxt: - '19574881057684356733946284215946569464410211018678168661028327420122678446653210056362495902735819742274128834330867933095119512313591151219353395069123546495720010325822330866859140765940839241212947354612836044244554152389691282543839111284006009168728161183863936810142428875817934316327118674532328892591410224676539770085459540786747902789677759379901079898127879301595929571621032704093287675668250862222728331030586585586110859977896767318814398026750215625180255041545607499673023585546720788973882263863911222208020438685873501025545464213035270207099419236974668665979962146355749687924650853489277747454993', - z: '18569464356833363098514177097771727133940629758890641648661259687745137028161881113251218061243607037717553708179509640909238773964066423807945164288256211132195919975343578956381001087353353060599758005375631247614777454313440511375923345538396573548499287265163879524050255226779884271432737062283353279122281220812931572456820130441114446870167673796490210349453498315913599982158253821945225264065364670730546176140788405935081171854642125236557475395879246419105888077042924382595999612137336915304205628167917473420377397118829734604949103124514367857266518654728464539418834291071874052392799652266418817991437', - }, - }, + value: {}, }, - credentialDefinitionId: `TL1EaPFCZ8Si5aUrqScBDt:3:CL:${schemaResponse.schemaMetadata.indyLedgerSeqNo}:TAG`, + credentialDefinitionId: didIndyCredentialDefinitionId, state: 'finished', }, registrationMetadata: {}, @@ -151,37 +147,202 @@ describe('IndySdkAnonCredsRegistry', () => { // Wait some time before resolving credential definition object await new Promise((res) => setTimeout(res, 1000)) - const credentialDefinitionResponse = await indySdkAnonCredsRegistry.getCredentialDefinition( + // Resolve using legacy credential definition id + const legacyCredentialDefinition = await indySdkAnonCredsRegistry.getCredentialDefinition( agent.context, - credentialDefinitionResult.credentialDefinitionState.credentialDefinitionId as string + legacyCredentialDefinitionId + ) + expect(legacyCredentialDefinition).toMatchObject({ + credentialDefinitionId: legacyCredentialDefinitionId, + credentialDefinition: { + issuerId: legacyIssuerId, + schemaId: legacySchemaId, + tag: 'TAG', + type: 'CL', + value: credentialDefinitionValue, + }, + credentialDefinitionMetadata: { + didIndyNamespace: 'pool:localtest', + }, + resolutionMetadata: {}, + }) + + // resolve using did indy credential definition id + const didIndyCredentialDefinition = await indySdkAnonCredsRegistry.getCredentialDefinition( + agent.context, + didIndyCredentialDefinitionId ) - expect(credentialDefinitionResponse).toMatchObject({ - credentialDefinitionId: `TL1EaPFCZ8Si5aUrqScBDt:3:CL:${schemaResponse.schemaMetadata.indyLedgerSeqNo}:TAG`, + expect(didIndyCredentialDefinition).toMatchObject({ + credentialDefinitionId: didIndyCredentialDefinitionId, credentialDefinition: { - issuerId: 'TL1EaPFCZ8Si5aUrqScBDt', - schemaId: `TL1EaPFCZ8Si5aUrqScBDt:2:test - 11:${dynamicVersion}`, + issuerId: didIndyIssuerId, + schemaId: didIndySchemaId, tag: 'TAG', type: 'CL', + value: credentialDefinitionValue, + }, + credentialDefinitionMetadata: { + didIndyNamespace: 'pool:localtest', + }, + resolutionMetadata: {}, + }) + + assertIndySdkWallet(agent.context.wallet) + + // We don't support creating a revocation registry using AFJ yet, so we directly use indy-sdk to register the revocation registry + const legacyRevocationRegistryId = `TL1EaPFCZ8Si5aUrqScBDt:4:TL1EaPFCZ8Si5aUrqScBDt:3:CL:${schemaResult.schemaMetadata.indyLedgerSeqNo}:TAG:CL_ACCUM:tag` + const didIndyRevocationRegistryId = `did:indy:pool:localtest:TL1EaPFCZ8Si5aUrqScBDt/anoncreds/v0/REV_REG_DEF/${schemaResult.schemaMetadata.indyLedgerSeqNo}/TAG/tag` + const revocationRegistryRequest = await indySdk.buildRevocRegDefRequest('TL1EaPFCZ8Si5aUrqScBDt', { + id: legacyRevocationRegistryId, + credDefId: legacyCredentialDefinitionId, + revocDefType: 'CL_ACCUM', + tag: 'tag', + value: { + issuanceType: 'ISSUANCE_BY_DEFAULT', + maxCredNum: 100, + publicKeys: { + accumKey: { + z: '1 1812B206EB395D3AEBD4BBF53EBB0FFC3371D8BD6175316AB32C1C5F65452051 1 22A079D49C5351EFDC1410C81A1F6D8B2E3B79CFF20A30690C118FE2050F72CB 1 0FFC28B923A4654E261DB4CB5B9BABEFCB4DB189B20F52412B0CC9CCCBB8A3B2 1 1EE967C43EF1A3F487061D21B07076A26C126AAF7712E7B5CF5A53688DDD5CC0 1 009ED4D65879CA81DA8227D34CEA3B759B4627E1E2FFB273E9645CD4F3B10F19 1 1CF070212E1E213AEB472F56EDFC9D48009796C77B2D8CC16F2836E37B8715C2 1 04954F0B7B468781BAAE3291DD0E6FFA7F1AF66CAA4094D37B24363CC34606FB 1 115367CB755E9DB18781B3825CB1AEE2C334558B2C038E13DF57BB57CE1CF847 1 110D37EC05862EE2757A7DF39E814876FC97376FF8105D2D29619CB575537BDE 1 13C559A9563FCE083B3B39AE7E8FCA4099BEF3A4C8C6672E543D521F9DA88F96 1 137D87CC22ACC1B6B8C20EABE59F6ED456A58FE4CBEEFDFC4FA9B87E3EF32D17 1 00A2A9711737AAF0404F35AE502887AC6172B2B57D236BD4A40B45F659BFC696', + }, + }, + tailsHash: 'HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + tailsLocation: '/var/folders/l3/xy8jzyvj4p5_d9g1123rt4bw0000gn/T/HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + }, + ver: '1.0', + }) + + await indySdkPoolService.submitWriteRequest(agent.context, pool, revocationRegistryRequest, signingKey) + + const legacyRevocationRegistryDefinition = await indySdkAnonCredsRegistry.getRevocationRegistryDefinition( + agent.context, + legacyRevocationRegistryId + ) + expect(legacyRevocationRegistryDefinition).toMatchObject({ + revocationRegistryDefinitionId: legacyRevocationRegistryId, + revocationRegistryDefinition: { + issuerId: legacyIssuerId, + revocDefType: 'CL_ACCUM', value: { - primary: { - n: '92511867718854414868106363741369833735017762038454769060600859608405811709675033445666654908195955460485998711087020152978597220168927505650092431295783175164390266561239892662085428655566792056852960599485298025843840058914610127716620252006466964070280255168745873592143068949458568751438337748294055976926080232538440619420568859737673474560851456027625679328271511966332808025880807996449998057729417608399774744254122385012832309402226532031122728445959276178939234308090390331654445053482963947804769291501664200141562885660084823885847247231002821472258218384342423605116504024514572826071246440130942849549441', - r: { - age: '676933340341980399002624386891134393471002096508227567343731826159610079436978196421307099268754545293545727546242372579987825752872485684085629459107300175443328323289748793060894500514926703654606851666031895448970879827423190730510730624784665299646624113512701254199984520803796529034094958026048762178753193812250643294518237843809104055653333871102658177900702978008644780459400512716361564897282969982554031820285585105004870317861287847206222714589633178648982299799311192432563797220854755882933052881306804544233529886513105815543097685128456041780804442879272476590077760678785460726492895806240870944398', - master_secret: - '57770757113548032970308439965749734133430520933173186296299026579579930337912607419798836831937319372744879560676750427054135869214212225572618340088847222727882935159356459822445182287686057012197046378986248048722180093079919306125315662058290895629438767985427829790980355162853804522854494960613869765167538645624719923127052541372069255024631093663068055100579264049925388231368871107383977060590248865498902704546409806115171120555709438784189721957301548212242748685629860268468247494986146122636455769804467583612610341632602695197189514316033637331733820369170763954604394734655429769801516997967996980978751', + maxCredNum: 100, + tailsHash: 'HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + tailsLocation: + '/var/folders/l3/xy8jzyvj4p5_d9g1123rt4bw0000gn/T/HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + publicKeys: { + accumKey: { + z: '1 1812B206EB395D3AEBD4BBF53EBB0FFC3371D8BD6175316AB32C1C5F65452051 1 22A079D49C5351EFDC1410C81A1F6D8B2E3B79CFF20A30690C118FE2050F72CB 1 0FFC28B923A4654E261DB4CB5B9BABEFCB4DB189B20F52412B0CC9CCCBB8A3B2 1 1EE967C43EF1A3F487061D21B07076A26C126AAF7712E7B5CF5A53688DDD5CC0 1 009ED4D65879CA81DA8227D34CEA3B759B4627E1E2FFB273E9645CD4F3B10F19 1 1CF070212E1E213AEB472F56EDFC9D48009796C77B2D8CC16F2836E37B8715C2 1 04954F0B7B468781BAAE3291DD0E6FFA7F1AF66CAA4094D37B24363CC34606FB 1 115367CB755E9DB18781B3825CB1AEE2C334558B2C038E13DF57BB57CE1CF847 1 110D37EC05862EE2757A7DF39E814876FC97376FF8105D2D29619CB575537BDE 1 13C559A9563FCE083B3B39AE7E8FCA4099BEF3A4C8C6672E543D521F9DA88F96 1 137D87CC22ACC1B6B8C20EABE59F6ED456A58FE4CBEEFDFC4FA9B87E3EF32D17 1 00A2A9711737AAF0404F35AE502887AC6172B2B57D236BD4A40B45F659BFC696', }, - rctxt: - '19574881057684356733946284215946569464410211018678168661028327420122678446653210056362495902735819742274128834330867933095119512313591151219353395069123546495720010325822330866859140765940839241212947354612836044244554152389691282543839111284006009168728161183863936810142428875817934316327118674532328892591410224676539770085459540786747902789677759379901079898127879301595929571621032704093287675668250862222728331030586585586110859977896767318814398026750215625180255041545607499673023585546720788973882263863911222208020438685873501025545464213035270207099419236974668665979962146355749687924650853489277747454993', - s: '80388543865249952799447792504739237616187770512259677275061283897050980768551818104137338144380636412773836688624071360386172349725818126495487584981520630638409717065318132420766896092370913800616033623618952639023946750307405126873476182540669638841562357523429245685476919178722373320218824590869735129801004394337640642997250464303104754942997839179333543643110326022824394934965538190976474473353762308333205671176627192797138375084260446324344637548455228161138089974447059481109651156379803576163576511072261388342837813901850712083922506433336723723235701670225584863772222447543742649328218950436824219992164', - z: '18569464356833363098514177097771727133940629758890641648661259687745137028161881113251218061243607037717553708179509640909238773964066423807945164288256211132195919975343578956381001087353353060599758005375631247614777454313440511375923345538396573548499287265163879524050255226779884271432737062283353279122281220812931572456820130441114446870167673796490210349453498315913599982158253821945225264065364670730546176140788405935081171854642125236557475395879246419105888077042924382595999612137336915304205628167917473420377397118829734604949103124514367857266518654728464539418834291071874052392799652266418817991437', }, }, + tag: 'tag', + credDefId: legacyCredentialDefinitionId, }, - credentialDefinitionMetadata: { + revocationRegistryDefinitionMetadata: { + issuanceType: 'ISSUANCE_BY_DEFAULT', + didIndyNamespace: 'pool:localtest', + }, + resolutionMetadata: {}, + }) + + const didIndyRevocationRegistryDefinition = await indySdkAnonCredsRegistry.getRevocationRegistryDefinition( + agent.context, + didIndyRevocationRegistryId + ) + expect(didIndyRevocationRegistryDefinition).toMatchObject({ + revocationRegistryDefinitionId: didIndyRevocationRegistryId, + revocationRegistryDefinition: { + issuerId: didIndyIssuerId, + revocDefType: 'CL_ACCUM', + value: { + maxCredNum: 100, + tailsHash: 'HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + tailsLocation: + '/var/folders/l3/xy8jzyvj4p5_d9g1123rt4bw0000gn/T/HLKresYcDSZYSKogq8wive4zyXNY84669MygftLFBG1i', + publicKeys: { + accumKey: { + z: '1 1812B206EB395D3AEBD4BBF53EBB0FFC3371D8BD6175316AB32C1C5F65452051 1 22A079D49C5351EFDC1410C81A1F6D8B2E3B79CFF20A30690C118FE2050F72CB 1 0FFC28B923A4654E261DB4CB5B9BABEFCB4DB189B20F52412B0CC9CCCBB8A3B2 1 1EE967C43EF1A3F487061D21B07076A26C126AAF7712E7B5CF5A53688DDD5CC0 1 009ED4D65879CA81DA8227D34CEA3B759B4627E1E2FFB273E9645CD4F3B10F19 1 1CF070212E1E213AEB472F56EDFC9D48009796C77B2D8CC16F2836E37B8715C2 1 04954F0B7B468781BAAE3291DD0E6FFA7F1AF66CAA4094D37B24363CC34606FB 1 115367CB755E9DB18781B3825CB1AEE2C334558B2C038E13DF57BB57CE1CF847 1 110D37EC05862EE2757A7DF39E814876FC97376FF8105D2D29619CB575537BDE 1 13C559A9563FCE083B3B39AE7E8FCA4099BEF3A4C8C6672E543D521F9DA88F96 1 137D87CC22ACC1B6B8C20EABE59F6ED456A58FE4CBEEFDFC4FA9B87E3EF32D17 1 00A2A9711737AAF0404F35AE502887AC6172B2B57D236BD4A40B45F659BFC696', + }, + }, + }, + tag: 'tag', + credDefId: didIndyCredentialDefinitionId, + }, + revocationRegistryDefinitionMetadata: { + issuanceType: 'ISSUANCE_BY_DEFAULT', didIndyNamespace: 'pool:localtest', }, resolutionMetadata: {}, }) + + // indySdk.buildRevRegEntry panics, so we just pass a custom request directly + const entryResponse = await indySdkPoolService.submitWriteRequest( + agent.context, + pool, + { + identifier: legacyIssuerId, + operation: { + revocDefType: 'CL_ACCUM', + revocRegDefId: legacyRevocationRegistryId, + type: '114', + value: { + accum: + '1 0000000000000000000000000000000000000000000000000000000000000000 1 0000000000000000000000000000000000000000000000000000000000000000 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000 1 0000000000000000000000000000000000000000000000000000000000000000 1 0000000000000000000000000000000000000000000000000000000000000000', + }, + }, + protocolVersion: 2, + reqId: Math.floor(Math.random() * 1000000), + }, + signingKey + ) + + const legacyRevocationStatusList = await indySdkAnonCredsRegistry.getRevocationStatusList( + agent.context, + legacyRevocationRegistryId, + entryResponse.result.txnMetadata.txnTime + ) + + expect(legacyRevocationStatusList).toMatchObject({ + resolutionMetadata: {}, + revocationStatusList: { + issuerId: legacyIssuerId, + currentAccumulator: + '1 0000000000000000000000000000000000000000000000000000000000000000 1 0000000000000000000000000000000000000000000000000000000000000000 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000 1 0000000000000000000000000000000000000000000000000000000000000000 1 0000000000000000000000000000000000000000000000000000000000000000', + revRegId: legacyRevocationRegistryId, + revocationList: [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + timestamp: entryResponse.result.txnMetadata.txnTime, + }, + revocationStatusListMetadata: { + didIndyNamespace: 'pool:localtest', + }, + }) + + const didIndyRevocationStatusList = await indySdkAnonCredsRegistry.getRevocationStatusList( + agent.context, + didIndyRevocationRegistryId, + entryResponse.result.txnMetadata.txnTime + ) + + expect(didIndyRevocationStatusList).toMatchObject({ + resolutionMetadata: {}, + revocationStatusList: { + issuerId: didIndyIssuerId, + currentAccumulator: + '1 0000000000000000000000000000000000000000000000000000000000000000 1 0000000000000000000000000000000000000000000000000000000000000000 2 095E45DDF417D05FB10933FFC63D474548B7FFFF7888802F07FFFFFF7D07A8A8 1 0000000000000000000000000000000000000000000000000000000000000000 1 0000000000000000000000000000000000000000000000000000000000000000 1 0000000000000000000000000000000000000000000000000000000000000000', + revRegId: didIndyRevocationRegistryId, + revocationList: [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + timestamp: entryResponse.result.txnMetadata.txnTime, + }, + revocationStatusListMetadata: { + didIndyNamespace: 'pool:localtest', + }, + }) }) }) diff --git a/packages/indy-sdk/tests/setupIndySdkModule.ts b/packages/indy-sdk/tests/setupIndySdkModule.ts index c0e8ee1313..b4a30f799a 100644 --- a/packages/indy-sdk/tests/setupIndySdkModule.ts +++ b/packages/indy-sdk/tests/setupIndySdkModule.ts @@ -2,7 +2,13 @@ import { DidsModule, KeyDidRegistrar, KeyDidResolver, utils } from '@aries-frame import indySdk from 'indy-sdk' import { genesisPath, taaVersion, taaAcceptanceMechanism } from '../../core/tests/helpers' -import { IndySdkModule, IndySdkModuleConfig, IndySdkSovDidRegistrar, IndySdkSovDidResolver } from '../src' +import { + IndySdkModule, + IndySdkModuleConfig, + IndySdkIndyDidRegistrar, + IndySdkSovDidResolver, + IndySdkIndyDidResolver, +} from '../src' export { indySdk } @@ -23,7 +29,7 @@ export const getIndySdkModuleConfig = () => export const getIndySdkModules = () => ({ indySdk: new IndySdkModule(getIndySdkModuleConfig()), dids: new DidsModule({ - registrars: [new IndySdkSovDidRegistrar(), new KeyDidRegistrar()], - resolvers: [new IndySdkSovDidResolver(), new KeyDidResolver()], + registrars: [new IndySdkIndyDidRegistrar(), new KeyDidRegistrar()], + resolvers: [new IndySdkSovDidResolver(), new IndySdkIndyDidResolver(), new KeyDidResolver()], }), }) diff --git a/packages/indy-sdk/tests/sov-did-resolver.e2e.test.ts b/packages/indy-sdk/tests/sov-did-resolver.e2e.test.ts index b1847a3f6d..ef220a59a3 100644 --- a/packages/indy-sdk/tests/sov-did-resolver.e2e.test.ts +++ b/packages/indy-sdk/tests/sov-did-resolver.e2e.test.ts @@ -1,8 +1,9 @@ -import type { IndySdkSovDidCreateOptions } from '../src/dids/IndySdkSovDidRegistrar' +import type { IndySdkIndyDidCreateOptions } from '../src' import { Agent, AriesFrameworkError, JsonTransformer, TypedArrayEncoder } from '@aries-framework/core' import { getAgentOptions, importExistingIndyDidFromPrivateKey, publicDidSeed } from '../../core/tests/helpers' +import { parseIndyDid } from '../src/dids/didIndyUtil' import { getIndySdkModules } from './setupIndySdkModule' @@ -25,12 +26,15 @@ describe('Indy SDK Sov DID resolver', () => { TypedArrayEncoder.fromString(publicDidSeed) ) - const createResult = await agent.dids.create({ - method: 'sov', + const createResult = await agent.dids.create({ + method: 'indy', options: { - submitterVerificationMethod: `did:sov:${unqualifiedSubmitterDid}#key-1`, + submitterDid: `did:indy:pool:localtest:${unqualifiedSubmitterDid}`, alias: 'Alias', role: 'TRUSTEE', + endpoints: { + endpoint: 'http://localhost:3000', + }, }, }) @@ -38,7 +42,10 @@ describe('Indy SDK Sov DID resolver', () => { await new Promise((res) => setTimeout(res, 1000)) if (!createResult.didState.did) throw new AriesFrameworkError('Unable to register did') - const didResult = await agent.dids.resolve(createResult.didState.did) + + const { id: unqualifiedDid } = parseIndyDid(createResult.didState.did) + const sovDid = `did:sov:${unqualifiedDid}` + const didResult = await agent.dids.resolve(sovDid) expect(JsonTransformer.toJSON(didResult)).toMatchObject({ didDocument: { @@ -47,29 +54,44 @@ describe('Indy SDK Sov DID resolver', () => { 'https://w3id.org/security/suites/ed25519-2018/v1', 'https://w3id.org/security/suites/x25519-2019/v1', ], - id: createResult.didState.did, + id: sovDid, alsoKnownAs: undefined, controller: undefined, verificationMethod: [ { type: 'Ed25519VerificationKey2018', - controller: createResult.didState.did, - id: `${createResult.didState.did}#key-1`, + controller: sovDid, + id: `${sovDid}#key-1`, publicKeyBase58: expect.any(String), }, { - controller: createResult.didState.did, + controller: sovDid, type: 'X25519KeyAgreementKey2019', - id: `${createResult.didState.did}#key-agreement-1`, + id: `${sovDid}#key-agreement-1`, publicKeyBase58: expect.any(String), }, ], capabilityDelegation: undefined, capabilityInvocation: undefined, - authentication: [`${createResult.didState.did}#key-1`], - assertionMethod: [`${createResult.didState.did}#key-1`], - keyAgreement: [`${createResult.didState.did}#key-agreement-1`], - service: undefined, + authentication: [`${sovDid}#key-1`], + assertionMethod: [`${sovDid}#key-1`], + keyAgreement: [`${sovDid}#key-agreement-1`], + service: [ + { + id: `${sovDid}#endpoint`, + serviceEndpoint: 'http://localhost:3000', + type: 'endpoint', + }, + { + id: `${sovDid}#did-communication`, + accept: ['didcomm/aip2;env=rfc19'], + priority: 0, + recipientKeys: [`${sovDid}#key-agreement-1`], + routingKeys: [], + serviceEndpoint: 'http://localhost:3000', + type: 'did-communication', + }, + ], }, didDocumentMetadata: {}, didResolutionMetadata: {