UNPKG

@sphereon/ssi-sdk.wellknown-did-issuer

Version:

261 lines (230 loc) • 11.4 kB
import { CredentialCorrelationType, CredentialRole, DigitalCredential } from '@sphereon/ssi-sdk.credential-store' import { CredentialMapper, parseDid } from '@sphereon/ssi-types' import { DomainLinkageCredential, IDidConfigurationResource, IssuanceCallback, ServiceTypesEnum, WellKnownDidIssuer as Issuer, } from '@sphereon/wellknown-dids-client' import { IAgentPlugin, IIdentifier, VerifiableCredential } from '@veramo/core' import { OrPromise } from '@veramo/utils' // @ts-ignore import { normalizeCredential } from 'did-jwt-vc' // @ts-ignore import { Service } from 'did-resolver/lib/resolver' import { Connection } from 'typeorm' import { v4 as uuidv4 } from 'uuid' import { createCredentialEntity, DidConfigurationResourceEntity, didConfigurationResourceFrom } from '../entities/DidConfigurationResourceEntity' import { schema } from '../index' import { IAddLinkedDomainsServiceArgs, IGetDidConfigurationResourceArgs, IIssueDidConfigurationResourceArgs, IIssueDomainLinkageCredentialArgs, IRegisterIssueCredentialArgs, IRemoveCredentialIssuanceArgs, ISaveDidConfigurationResourceArgs, IWellKnownDidIssuer, IWellKnownDidIssuerOptionsArgs, RequiredContext, } from '../types/IWellKnownDidIssuer' import { RegulationType } from '@sphereon/ssi-sdk.data-store-types' /** * {@inheritDoc IWellKnownDidIssuer} */ export class WellKnownDidIssuer implements IAgentPlugin { readonly schema = schema.IWellKnownDidVerifier readonly methods: IWellKnownDidIssuer = { addLinkedDomainsService: this.addLinkedDomainsService.bind(this), getDidConfigurationResource: this.getDidConfigurationResource.bind(this), issueDidConfigurationResource: this.issueDidConfigurationResource.bind(this), issueDomainLinkageCredential: this.issueDomainLinkageCredential.bind(this), registerCredentialIssuance: this.registerCredentialIssuance.bind(this), removeCredentialIssuance: this.removeCredentialIssuance.bind(this), saveDidConfigurationResource: this.saveDidConfigurationResource.bind(this), } private readonly credentialIssuances: Record<string, IssuanceCallback> private readonly didConfigurationResourceRelations = ['linkedDids'] constructor( private dbConnection: OrPromise<Connection>, args?: IWellKnownDidIssuerOptionsArgs, ) { this.credentialIssuances = (args && args.credentialIssuances) || {} } /** {@inheritDoc IWellKnownDidIssuer.registerSignatureVerification} */ private async registerCredentialIssuance(args: IRegisterIssueCredentialArgs, context: RequiredContext): Promise<void> { if (this.credentialIssuances[args.callbackName] !== undefined) { return Promise.reject(new Error(`Credential issuance with callbackName: ${args.callbackName} already present`)) } this.credentialIssuances[args.callbackName] = args.credentialIssuance } /** {@inheritDoc IWellKnownDidIssuer.removeSignatureVerification} */ private async removeCredentialIssuance(args: IRemoveCredentialIssuanceArgs, context: RequiredContext): Promise<boolean> { return delete this.credentialIssuances[args.callbackName] } /** {@inheritDoc IWellKnownDidIssuer.issueDidConfigurationResource} */ private async issueDidConfigurationResource( args: IIssueDidConfigurationResourceArgs, context: RequiredContext, ): Promise<IDidConfigurationResource> { if (!args.issuances.every((issuance: IIssueDomainLinkageCredentialArgs) => issuance.origin === args.issuances[0].origin)) { return Promise.reject(Error('All verifiableCredentials should be issued for the same origin')) } // TODO We should combine all origins into one service when we update to Veramo 3.1.6.next-165 or higher, as then we can support multiple origins const addServices = args.issuances.map((issuance: IIssueDomainLinkageCredentialArgs) => this.addLinkedDomainsService( { did: issuance.did, origin: issuance.origin, serviceId: issuance.serviceId, }, context, ), ) return Promise.all(addServices).then(async () => new Issuer() .issueDidConfigurationResource({ issuances: await this.mapIssuances(args.issuances), issueCallback: typeof args.credentialIssuance === 'string' ? await this.getCredentialIssuance(args.credentialIssuance) : args.credentialIssuance, }) .then(async (didConfigurationResource: IDidConfigurationResource) => { if (args.save) { // TODO add support for multiple origins when we upgrade Veramo version await this.saveDidConfigurationResource({ origin: args.issuances[0].origin, didConfigurationResource }, context) } return didConfigurationResource }) .catch((error: Error) => Promise.reject(Error(`Unable to issue DID configuration resource. Error: ${error.message}`))), ) } /** {@inheritDoc IWellKnownDidIssuer.saveDidConfigurationResource} */ public async saveDidConfigurationResource(args: ISaveDidConfigurationResourceArgs, context: RequiredContext): Promise<void> { const didConfigurationEntity = { origin: args.origin, context: args.didConfigurationResource['@context'], linkedDids: args.didConfigurationResource.linked_dids.map((credential: DomainLinkageCredential) => createCredentialEntity(this.normalizeCredential(credential)), ), } await (await this.dbConnection).getRepository(DidConfigurationResourceEntity).save(didConfigurationEntity, { transaction: true }) } /** {@inheritDoc IWellKnownDidIssuer.getDidConfigurationResource} */ public async getDidConfigurationResource(args: IGetDidConfigurationResourceArgs, context: RequiredContext): Promise<IDidConfigurationResource> { const result = await (await this.dbConnection).getRepository(DidConfigurationResourceEntity).findOne({ where: { origin: args.origin }, relations: this.didConfigurationResourceRelations, }) if (!result) { return Promise.reject(Error(`No DID configuration resource found for origin: ${args.origin}`)) } return didConfigurationResourceFrom(result) } /** {@inheritDoc IWellKnownDidIssuer.issueDomainLinkageCredential} */ public async issueDomainLinkageCredential(args: IIssueDomainLinkageCredentialArgs, context: RequiredContext): Promise<DomainLinkageCredential> { const did: string = parseDid(args.did).did if (new URL(args.origin).origin !== args.origin) { return Promise.reject(Error(`Origin ${args.origin} is not valid`)) } if (new URL(args.origin).protocol !== 'https:') { return Promise.reject(Error(`Origin ${args.origin} is not a https URL`)) } if (args.issuanceDate && isNaN(Date.parse(args.issuanceDate))) { return Promise.reject(Error(`IssuanceDate ${args.issuanceDate} is not a valid date`)) } if (isNaN(Date.parse(args.expirationDate))) { return Promise.reject(Error(`ExpirationDate ${args.expirationDate} is not a valid date`)) } const credentialIssuance: IssuanceCallback = typeof args.credentialIssuance === 'string' ? await this.getCredentialIssuance(args.credentialIssuance) : (args.credentialIssuance as IssuanceCallback) return new Issuer() .issueDomainLinkageCredential({ did, origin: args.origin, issuanceDate: args.issuanceDate, expirationDate: args.expirationDate, options: args.options, issueCallback: credentialIssuance, }) .then(async (credential: DomainLinkageCredential) => { if (args.save) { await this.saveDomainLinkageCredential(credential, context) } return credential }) .catch((error: Error) => Promise.reject(Error(`Unable to issue domain linkage credential for DID: ${did}. Error: ${error.message}`))) } /** {@inheritDoc IWellKnownDidIssuer.addLinkedDomainsService} */ public async addLinkedDomainsService(args: IAddLinkedDomainsServiceArgs, context: RequiredContext): Promise<void> { const did: string = parseDid(args.did).did if (new URL(args.origin).origin !== args.origin) { return Promise.reject(Error(`Origin ${args.origin} is not valid`)) } if (new URL(args.origin).protocol !== 'https:') { return Promise.reject(Error(`Origin ${args.origin} is not a https URL`)) } context.agent .didManagerGet({ did }) .catch(() => Promise.reject(Error('DID cannot be found'))) .then(async (identifier: IIdentifier) => { if ( !identifier.services || identifier.services.filter( // TODO we should also check for the origins in the serviceEndpoint objects when we start supporting multiple origins (service: Service) => service.type === ServiceTypesEnum.LINKED_DOMAINS && service.serviceEndpoint === args.origin, ).length === 0 ) { await context.agent.didManagerAddService({ did: identifier.did, service: { id: args.serviceId || uuidv4(), type: ServiceTypesEnum.LINKED_DOMAINS, // TODO We should support a serviceEndpoint object here when we update to Veramo 3.1.6.next-165 or higher, as then we can support multiple origins serviceEndpoint: args.origin, }, }) } }) .catch((error: Error) => Promise.reject(Error(`Unable to add LinkedDomains service to DID: ${args.did}. Error: ${error.message}`))) } private async getCredentialIssuance(callbackName: string): Promise<IssuanceCallback> { if (this.credentialIssuances[callbackName] === undefined) { return Promise.reject(new Error(`Credential issuance not found for callbackName: ${callbackName}`)) } return this.credentialIssuances[callbackName] } private async saveDomainLinkageCredential(credential: DomainLinkageCredential, context: RequiredContext): Promise<DigitalCredential> { const vc = this.normalizeCredential(credential) return context.agent.crsAddCredential({ credential: { rawDocument: JSON.stringify(vc), credentialRole: CredentialRole.ISSUER, regulationType: RegulationType.NON_REGULATED, // FIXME funke kmsKeyRef: 'FIXME', // FIXME funke identifierMethod: 'did', issuerCorrelationId: CredentialMapper.issuerCorrelationIdFromIssuerType(vc.issuer), issuerCorrelationType: CredentialCorrelationType.DID, subjectCorrelationId: CredentialMapper.issuerCorrelationIdFromIssuerType(vc.issuer), // FIXME get separate did for subject subjectCorrelationType: CredentialCorrelationType.DID, }, }) } private normalizeCredential(credential: DomainLinkageCredential): VerifiableCredential { return typeof credential === 'string' ? normalizeCredential(credential) : credential } private async mapIssuances(issuances: Array<IIssueDomainLinkageCredentialArgs>): Promise<Array<IIssueDomainLinkageCredentialArgs>> { const promises = issuances.map(async (issuance: IIssueDomainLinkageCredentialArgs) => { return { ...issuance, issueCallback: typeof issuance.credentialIssuance === 'string' ? await this.getCredentialIssuance(issuance.credentialIssuance) : issuance.credentialIssuance, } }) return Promise.all(promises) } }