@sphereon/ssi-sdk.wellknown-did-issuer
Version:
261 lines (230 loc) • 11.4 kB
text/typescript
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'
/**
* {@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)
}
}