diff --git a/agent-swagger.json b/agent-swagger.json index 36a7939c1d8ff5118882a4bfcb80090733e8be13..29bddefdf9535ff4d2ad7cb4b5feb722bd1ad2d0 100644 --- a/agent-swagger.json +++ b/agent-swagger.json @@ -96,7 +96,14 @@ ], "responses": { "200": { - "description": "" + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateInvitationResponseDto" + } + } + } } } }, @@ -924,6 +931,27 @@ } } }, + "/api/v1/created-dids": { + "get": { + "operationId": "RestController_getCreatedDids", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DidRecordDto" + } + } + } + } + } + } + } + }, "/api/v1/resolve-did": { "post": { "operationId": "RestController_resolveDid", @@ -1567,6 +1595,44 @@ "proofId", "proofUrl" ] + }, + "DidRecordDto": { + "type": "object", + "properties": { + "did": { + "type": "string" + }, + "role": { + "type": "string", + "enum": [ + "created", + "received" + ] + }, + "method": { + "type": "string" + }, + "tags": { + "type": "object" + }, + "id": { + "type": "string" + }, + "createdAt": { + "format": "date-time", + "type": "string" + }, + "updatedAt": { + "format": "date-time", + "type": "string" + } + }, + "required": [ + "did", + "role", + "method", + "tags" + ] } } } diff --git a/libs/askar/src/agent.utils.ts b/libs/askar/src/agent.utils.ts index 61333fba9816d15ffa9c7cefc95702e1405d50d3..ca9c060d64bf1601ed07a70028a4fcd25da17b17 100644 --- a/libs/askar/src/agent.utils.ts +++ b/libs/askar/src/agent.utils.ts @@ -27,6 +27,10 @@ import { CredentialEventTypes, CredentialState, ProofExchangeRecord, + JsonTransformer, + DidDocument, + WebDidResolver, + JwkDidResolver, } from "@aries-framework/core"; import { AnonCredsCredentialFormatService, @@ -45,7 +49,7 @@ import { anoncreds } from "@hyperledger/anoncreds-nodejs"; import { indyVdr } from "@hyperledger/indy-vdr-nodejs"; import { AskarModule } from "@aries-framework/askar"; import { ariesAskar } from "@hyperledger/aries-askar-nodejs"; -import { Key as C, KeyAlgs } from "@hyperledger/aries-askar-shared"; +import { Key as AskarKey, KeyAlgs } from "@hyperledger/aries-askar-shared"; import { IConfAgent } from "@ocm-engine/config"; import axios from "axios"; import { @@ -68,6 +72,7 @@ export type SubjectMessage = { replySubject?: Subject<SubjectMessage>; }; import { Request, Response, Express } from "express"; +import url from "url"; export const importDidsToWallet = async ( agent: Agent, @@ -96,18 +101,18 @@ export const generateKey = async ({ throw Error("No seed provided"); } - const privateKey = TypedArrayEncoder.fromString(seed); + const seedBuffer = TypedArrayEncoder.fromString(seed); try { return await agent.wallet.createKey({ - seed: privateKey, + seed: seedBuffer, keyType: KeyType.Ed25519, }); } catch (e) { if (e instanceof WalletKeyExistsError) { - const c = C.fromSecretBytes({ + const c = AskarKey.fromSeed({ algorithm: KeyAlgs.Ed25519, - secretKey: TypedArrayEncoder.fromString(seed), + seed: seedBuffer, }); return Key.fromPublicKey(c.publicBytes, KeyType.Ed25519); @@ -121,6 +126,83 @@ export const generateKey = async ({ } }; +export const generateDidWeb = async ({ + seed, + agent, + peerAddress +}: { + seed: string; + agent: Agent; + peerAddress: string; +}) => { + console.log("Generating did web"); + const pubKey = await generateKey({ seed, agent }); + + const parsedUrl = url.parse(peerAddress); + let hostname = parsedUrl.hostname!; + const port = parsedUrl.port; + let pathname = parsedUrl.pathname?.replace(/^\/+|\/+$/g, ''); + + // If port is specified, encode it + if (port) { + hostname += `%3A${port}`; + } + // Convert URLs to 'did:web' form + let didWeb = `did:web:${hostname}`; + if (pathname) { + didWeb += `:${pathname.replace(/\//g, ':')}`; + } + + const verificationMethodKey0Id = `${didWeb}#jwt-key0`; + + const jsonDidDoc = { + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/ed25519-2018/v1" + ], + "id": didWeb, + "verificationMethod": [ + { + "id": verificationMethodKey0Id, + "type": "Ed25519VerificationKey2018", + "controller": didWeb, + "publicKeyBase58": pubKey.publicKeyBase58 + }, + ], + "authentication": [ + verificationMethodKey0Id + ], + "assertionMethod": [ + verificationMethodKey0Id + ], + "keyAgreement": [ + verificationMethodKey0Id + ] + }; + + const didDocumentInstance = JsonTransformer.fromJSON(jsonDidDoc, DidDocument) + + const recordId = "did:web"; + const existingRecord = await agent.genericRecords.findById(recordId); + if (existingRecord) { + await agent.genericRecords.deleteById(recordId); + } + await agent.genericRecords.save({ + id: recordId, + content: jsonDidDoc + }); + + await agent.dids.import({ + did: didWeb, + didDocument: didDocumentInstance, + overwrite: false + }); + + console.log("Generated did:web"); + console.log(didWeb); + console.log(JSON.stringify(didDocumentInstance.toJSON(), null, 2)); +}; + export const generateDidFromKey = (key: Key): string => { if (!key) { throw new Error("Key not provided"); @@ -171,6 +253,8 @@ export const getAskarAnonCredsIndyModules = (networks: any) => { new IndyVdrIndyDidResolver(), new KeyDidResolver(), new PeerDidResolver(), + new WebDidResolver(), + new JwkDidResolver() ], }), askar: new AskarModule({ @@ -486,3 +570,38 @@ export const attachShortUrlHandler = (server: Express, agent: Agent): void => { }, ); }; + +export const attachDidWebHandler = (server: Express, agent: Agent, agentPeerAddress: string): void => { + const parsedUrl = url.parse(agentPeerAddress); + const pathname = parsedUrl.pathname?.replace(/^\/+|\/+$/g, ''); + + let serverDidWebPath: string; + if (pathname) { + serverDidWebPath = `/did.json`; + } else { + serverDidWebPath = "/.well-known/did.json"; + } + + console.log('Listen did web requests on path ' + serverDidWebPath); + server.get( + serverDidWebPath, + async (req: Request, res: Response) => { + try { + const didWebRecord = await agent.genericRecords.findById("did:web"); + + if (!didWebRecord) { + return res.status(404).send("Not found"); + } + + const didWebDoc = didWebRecord.content; + + return res + .header("Content-Type", "application/json") + .send(didWebDoc); + } catch (error) { + console.error(error); + return res.status(500).send("Internal Server Error"); + } + }, + ); +}; diff --git a/libs/askar/src/askar-rest/rest.controller.ts b/libs/askar/src/askar-rest/rest.controller.ts index a2fc06e3d1134ca81392622af07aeb7a11137fab..29c8cf037dedac4f40620a23f43a641bb4b8da7d 100644 --- a/libs/askar/src/askar-rest/rest.controller.ts +++ b/libs/askar/src/askar-rest/rest.controller.ts @@ -25,6 +25,7 @@ import { CreateInvitationRequestDto, InvitationFilterDto, AcceptInvitationRequestDto, + DidRecordDto, } from "@ocm-engine/dtos"; import { AllExceptionsHandler } from "./exception.handler"; import { DidResolutionResult } from "@aries-framework/core"; @@ -215,6 +216,11 @@ export class RestController { return this.agentService.deleteProofById(proofRecordId); } + @Get("/created-dids") + async getCreatedDids(): Promise<DidRecordDto[]> { + return this.agentService.getCreatedDids(); + } + @Post("/resolve-did") async resolveDid(@Body() dto: IdReqDto): Promise<DidResolutionResult> { return this.agentService.resolve(dto.id); diff --git a/libs/askar/src/askar/agent.service.ts b/libs/askar/src/askar/agent.service.ts index 0dd9a78f93f685060e9f9ba11ac5b9142361f4eb..11605cd6e83caf8db1056c2a6683da82df247e23 100644 --- a/libs/askar/src/askar/agent.service.ts +++ b/libs/askar/src/askar/agent.service.ts @@ -27,6 +27,7 @@ import { ProofFormatDataDto, CreateInvitationRequestDto, InvitationFilterDto, + DidRecordDto, } from "@ocm-engine/dtos"; import { AutoAcceptCredential, @@ -979,4 +980,19 @@ export class AgentService { deleteMessageById = async (id: string): Promise<void> => { await this.askar.agent.basicMessages.deleteById(id); }; + + getCreatedDids = async (): Promise<DidRecordDto[]> => { + const didRecords = await this.askar.agent.dids.getCreatedDids(); + return didRecords.map(p => { + const dto = new DidRecordDto(); + const tags = p.getTags(); + + dto.did = p.did; + dto.role = p.role; + dto.method = tags.method; + dto.tags = tags; + + return dto; + }); + }; } diff --git a/libs/askar/src/askar/askar.service.ts b/libs/askar/src/askar/askar.service.ts index f8f8664ae3ea09a2a84c6209bb469c48854e9e88..846af0559433776f1533c7bff2221fc92e740e9e 100644 --- a/libs/askar/src/askar/askar.service.ts +++ b/libs/askar/src/askar/askar.service.ts @@ -26,8 +26,10 @@ import { getAskarAnonCredsIndyModules, importDidsToWallet, attachShortUrlHandler, + attachDidWebHandler, setupEventBehaviorSubjects, setupSubjectTransports, + generateDidWeb, } from "../agent.utils"; import { IConfAgent } from "@ocm-engine/config"; import { BehaviorSubject } from "rxjs"; @@ -78,6 +80,7 @@ export class AskarService implements OnModuleInit, OnModuleDestroy { //handler for short url invitations, look at agent service createInvitation attachShortUrlHandler(this.server, this.agent); + attachDidWebHandler(this.server, this.agent, this.agentConfig.agentPeerAddress); this.agent.registerInboundTransport( new HttpInboundTransport({ @@ -111,6 +114,19 @@ export class AskarService implements OnModuleInit, OnModuleDestroy { throw new Error("agent not initialized"); } + const didWebs = await this.agent.dids.getCreatedDids({ method: "web" }); + if (didWebs.length) { + for (const didsKey in didWebs) { + this.logger.debug(`agent already have ${didWebs[didsKey].did} registered`); + } + } else { + await generateDidWeb({ + agent: this.agent, + seed: this.agentConfig.agentDidSeed, + peerAddress: this.agentConfig.agentPeerAddress + }); + } + const dids = await this.agent.dids.getCreatedDids({ method: "indy" }); if (dids.length) { diff --git a/libs/dtos/src/dtos/generics/did.record.dto.ts b/libs/dtos/src/dtos/generics/did.record.dto.ts new file mode 100644 index 0000000000000000000000000000000000000000..7a3eac934bdc46718415f94fdc0ea4a371e86f2b --- /dev/null +++ b/libs/dtos/src/dtos/generics/did.record.dto.ts @@ -0,0 +1,19 @@ +import { IsNotEmpty, IsString } from "class-validator"; +import { BaseRecordDto } from "./base.record.dto"; +import { DidDocumentRole } from "@aries-framework/core"; + +export class DidRecordDto extends BaseRecordDto { + @IsNotEmpty() + @IsString() + did: string; + + @IsNotEmpty() + @IsString() + role: DidDocumentRole; + + @IsString() + @IsNotEmpty() + method: string; + + tags: unknown; +} diff --git a/libs/dtos/src/index.ts b/libs/dtos/src/index.ts index 8589661be71e8dc7b9363e6a29083e6eaa5b2df3..f5ff5ecf5c5b62b31923e102469da191a3c06f3f 100644 --- a/libs/dtos/src/index.ts +++ b/libs/dtos/src/index.ts @@ -12,6 +12,7 @@ export * from "./dtos/generics/schema.record.dto"; export * from "./dtos/generics/message.record.dto"; export * from "./dtos/generics/message.filter.dto"; export * from "./dtos/generics/invitation.filter.dto"; +export * from "./dtos/generics/did.record.dto"; export * from "./dtos/requests/accept.proof.dto"; export * from "./dtos/requests/accept.credential.dto";