diff --git a/packages/dids/src/dht.ts b/packages/dids/src/dht.ts index 2f4d637c4..39185a8ad 100644 --- a/packages/dids/src/dht.ts +++ b/packages/dids/src/dht.ts @@ -1,37 +1,209 @@ -import dns, {AUTHORITATIVE_ANSWER, Packet, TxtAnswer} from 'dns-packet'; -import Encoder from '@decentralized-identity/ion-sdk/dist/lib/Encoder.js'; -import {Pkarr, SignedPacket, z32} from 'pkarr'; -import type {PublicKeyJwk, Web5Crypto} from '@web5/crypto'; -import {Jose} from '@web5/crypto'; -import type {DidDocument} from './types.js'; +import type { Packet, TxtAnswer } from 'dns-packet'; +import type { PublicKeyJwk, Web5Crypto} from '@web5/crypto'; + +import { Jose } from '@web5/crypto'; +import { Convert } from '@web5/common'; +import { Pkarr, SignedPacket, z32 } from 'pkarr'; +import dns, { AUTHORITATIVE_ANSWER } from 'dns-packet'; + +import type { DidDocument } from './types.js'; const PKARR_RELAY = 'https://diddht.tbddev.org'; const TTL = 7200; +/** + * A class to handle operations related to DHT-based Decentralized Identifiers (DIDs). + * It provides methods to: + * - Parse a DNS packet into a DID Document. + * - Retrieve a DID Document from the DHT. + * - Publish a DID Document to the DHT. + * - Convert a DID Document to a DNS packet. + * + * The class assumes that DIDs and DID Documents are compliant with the did:dht specification. + */ export class DidDht { /** - * Publishes a DID Document to the DHT - * @param keypair The keypair to sign the document with - * @param did The DID Document to publish - * @param relay The relay to use to retrieve the document; defaults to PKARR_RELAY - */ - public static async publishDidDocument(keypair: Web5Crypto.CryptoKeyPair, did: DidDocument, relay: string=PKARR_RELAY): Promise { - const packet = await DidDht.toDnsPacket(did); + * Parses a DNS packet into a DID Document. + * @param did The DID of the document. + * @param packet A DNS packet to parse into a DID Document. + * @returns A Promise that resolves to the parsed DidDocument. + */ + public static async fromDnsPacket({ did, packet }: { + did: string, + packet: Packet + }): Promise { + const document: Partial = { + id: did, + }; + + const keyLookup = new Map(); + + for (const answer of packet.answers) { + if (answer.type !== 'TXT') continue; + + const dataStr = answer.data?.toString(); + // Extracts 'k' or 's' from "_k0._did" or "_s0._did" + const recordType = answer.name?.split('.')[0].substring(1, 2); + + /*eslint-disable no-case-declarations*/ + switch (recordType) { + case 'k': { + const { id, t, k } = DidDht.parseTxtData({ data: dataStr }); + const keyConfigurations: { [keyType: string]: Partial } = { + '0': { + crv : 'Ed25519', + kty : 'OKP', + alg : 'EdDSA' + }, + '1': { + crv : 'secp256k1', + kty : 'EC', + alg : 'ES256K' + } + }; + const keyConfig = keyConfigurations[t]; + if (!keyConfig) { + throw new Error('Unsupported key type'); + } + + const publicKeyJwk = await Jose.keyToJwk({ + ...keyConfig, + kid : id, + keyMaterial : Convert.base64Url(k).toUint8Array(), + keyType : 'public' + }) as PublicKeyJwk; + + if (!document.verificationMethod) { + document.verificationMethod = []; + } + document.verificationMethod.push({ + id : `${did}#${id}`, + type : 'JsonWebKey2020', + controller : did, + publicKeyJwk : publicKeyJwk, + }); + keyLookup.set(answer.name, id); + + break; + } + + case 's': { + const {id: sId, t: sType, uri} = DidDht.parseTxtData({ data: dataStr }); + + if (!document.service) { + document.service = []; + } + document.service.push({ + id : `${did}#${sId}`, + type : sType, + serviceEndpoint : uri + }); + + break; + } + } + } + + // Extract relationships from root record + const didSuffix = did.split('did:dht:')[1]; + const potentialRootNames = ['_did', `_did.${didSuffix}`]; + + let actualRootName = null; + const root = packet.answers + .filter(answer => { + if (potentialRootNames.includes(answer.name)) { + actualRootName = answer.name; + return true; + } + return false; + }) as dns.TxtAnswer[]; + + if (root.length === 0) { + throw new Error('No root record found'); + } + + if (root.length > 1) { + throw new Error('Multiple root records found'); + } + const singleRoot = root[0] as dns.TxtAnswer; + const rootRecord = singleRoot.data?.toString().split(';'); + rootRecord?.forEach(record => { + const [type, ids] = record.split('='); + let idList = ids?.split(',').map(id => `#${keyLookup.get(`_${id}.${actualRootName}`)}`); + switch (type) { + case 'auth': + document.authentication = idList; + break; + case 'asm': + document.assertionMethod = idList; + break; + case 'agm': + document.keyAgreement = idList; + break; + case 'inv': + document.capabilityInvocation = idList; + break; + case 'del': + document.capabilityDelegation = idList; + break; + } + }); + + return document as DidDocument; + } + + /** + * Retrieves a DID Document from the DHT. + * + * @param did The DID of the document to retrieve. + * @param relay The relay to use to retrieve the document; defaults to `PKARR_RELAY`. + * @returns A Promise that resolves to the retrieved DidDocument. + */ + public static async getDidDocument({ did, relay = PKARR_RELAY }: { + did: string, + relay?: string + }): Promise { + const didFragment = did.replace('did:dht:', ''); + const publicKeyBytes = new Uint8Array(z32.decode(didFragment)); + const resolved = await Pkarr.relayGet(relay, publicKeyBytes); + if (resolved) { + return await DidDht.fromDnsPacket({ did, packet: resolved.packet() }); + } + throw new Error('No packet found'); + } + + /** + * Publishes a DID Document to the DHT. + * + * @param keyPair The key pair to sign the document with. + * @param didDocument The DID Document to publish. + * @param relay The relay to use to retrieve the document; defaults to `PKARR_RELAY`. + * @returns A boolean indicating the success of the publishing operation. + */ + public static async publishDidDocument({ keyPair, didDocument, relay = PKARR_RELAY }: { + didDocument: DidDocument, + keyPair: Web5Crypto.CryptoKeyPair, + relay?: string + }): Promise { + const packet = await DidDht.toDnsPacket({ didDocument }); const pkarrKeypair = { - publicKey : keypair.publicKey.material, - secretKey : new Uint8Array([...keypair.privateKey.material, ...keypair.publicKey.material]) + publicKey : keyPair.publicKey.material, + secretKey : new Uint8Array([...keyPair.privateKey.material, ...keyPair.publicKey.material]) }; const signedPacket = SignedPacket.fromPacket(pkarrKeypair, packet); const results = await Pkarr.relayPut(relay, signedPacket); + return results.ok; } /** - * Converts a DID Document to a DNS packet according to the did:dht spec - * @param document The DID Document to convert - */ - public static async toDnsPacket(document: DidDocument): Promise { + * Converts a DID Document to a DNS packet according to the did:dht spec. + * + * @param didDocument The DID Document to convert. + * @returns A DNS packet converted from the DID Document. + */ + public static async toDnsPacket({ didDocument }: { didDocument: DidDocument }): Promise { const packet: Partial = { id : 0, type : 'response', @@ -45,10 +217,10 @@ export class DidDht { const keyLookup = new Map(); // Add key records for each verification method - for (const vm of document.verificationMethod) { - const index = document.verificationMethod.indexOf(vm); + for (const vm of didDocument.verificationMethod) { + const index = didDocument.verificationMethod.indexOf(vm); const recordIdentifier = `k${index}`; - let vmId = DidDht.identifierFragment(vm.id); + let vmId = DidDht.identifierFragment({ identifier: vm.id }); keyLookup.set(vmId, recordIdentifier); let keyType: number; @@ -63,8 +235,9 @@ export class DidDht { keyType = 0; // Default value or throw an error if needed } - const cryptoKey = await Jose.jwkToCryptoKey({key: vm.publicKeyJwk}); - const keyBase64Url = Encoder.encode(cryptoKey.material); + const cryptoKey = await Jose.jwkToCryptoKey({ key: vm.publicKeyJwk }); + const keyBase64Url = Convert.uint8Array(cryptoKey.material).toBase64Url(); + const keyRecord: TxtAnswer = { type : 'TXT', name : `_${recordIdentifier}._did`, @@ -77,9 +250,9 @@ export class DidDht { } // Add service records - document.service?.forEach((service, index) => { + didDocument.service?.forEach((service, index) => { const recordIdentifier = `s${index}`; - let sId = DidDht.identifierFragment(service.id); + let sId = DidDht.identifierFragment({ identifier: service.id }); const serviceRecord: TxtAnswer = { type : 'TXT', name : `_${recordIdentifier}._did`, @@ -100,45 +273,45 @@ export class DidDht { } // add verification relationships - if (document.authentication) { - const authIds: string[] = document.authentication - .map(id => DidDht.identifierFragment(id)) + if (didDocument.authentication) { + const authIds: string[] = didDocument.authentication + .map(id => DidDht.identifierFragment({ identifier: id })) .filter(id => keyLookup.has(id)) .map(id => keyLookup.get(id) as string); if (authIds.length) { rootRecord.push(`auth=${authIds.join(',')}`); } } - if (document.assertionMethod) { - const authIds: string[] = document.assertionMethod - .map(id => DidDht.identifierFragment(id)) + if (didDocument.assertionMethod) { + const authIds: string[] = didDocument.assertionMethod + .map(id => DidDht.identifierFragment({ identifier: id })) .filter(id => keyLookup.has(id)) .map(id => keyLookup.get(id) as string); if (authIds.length) { rootRecord.push(`asm=${authIds.join(',')}`); } } - if (document.keyAgreement) { - const authIds: string[] = document.keyAgreement - .map(id => DidDht.identifierFragment(id)) + if (didDocument.keyAgreement) { + const authIds: string[] = didDocument.keyAgreement + .map(id => DidDht.identifierFragment({ identifier: id })) .filter(id => keyLookup.has(id)) .map(id => keyLookup.get(id) as string); if (authIds.length) { rootRecord.push(`agm=${authIds.join(',')}`); } } - if (document.capabilityInvocation) { - const authIds: string[] = document.capabilityInvocation - .map(id => DidDht.identifierFragment(id)) + if (didDocument.capabilityInvocation) { + const authIds: string[] = didDocument.capabilityInvocation + .map(id => DidDht.identifierFragment({ identifier: id })) .filter(id => keyLookup.has(id)) .map(id => keyLookup.get(id) as string); if (authIds.length) { rootRecord.push(`inv=${authIds.join(',')}`); } } - if (document.capabilityDelegation) { - const authIds: string[] = document.capabilityDelegation - .map(id => DidDht.identifierFragment(id)) + if (didDocument.capabilityDelegation) { + const authIds: string[] = didDocument.capabilityDelegation + .map(id => DidDht.identifierFragment({ identifier: id })) .filter(id => keyLookup.has(id)) .map(id => keyLookup.get(id) as string); if (authIds.length) { @@ -158,154 +331,26 @@ export class DidDht { } /** - * Extracts the fragment from a DID - * @param identifier The DID to extract the fragment from - */ - private static identifierFragment(identifier: string): string { - return identifier.includes('#') ? identifier.substring(identifier.indexOf('#') + 1) : identifier; - } - - /** - * Retrieves a DID Document from the DHT - * @param did The DID of the document to retrieve - * @param relay The relay to use to retrieve the document; defaults to PKARR_RELAY + * Extracts the fragment from a DID. + * + * @param identifier The DID to extract the fragment from. + * @returns The fragment from the DID or the complete DID if no fragment exists. */ - public static async getDidDocument(did: string, relay: string=PKARR_RELAY): Promise { - const didFragment = did.replace('did:dht:', ''); - const publicKeyBytes = new Uint8Array(z32.decode(didFragment)); - const resolved = await Pkarr.relayGet(relay, publicKeyBytes); - if (resolved) { - return await DidDht.fromDnsPacket(did, resolved.packet()); - } - throw new Error('No packet found'); + private static identifierFragment({ identifier }: { identifier: string }): string { + return identifier.includes('#') ? identifier.substring(identifier.indexOf('#') + 1) : identifier; } /** - * Parses a DNS packet into a DID Document - * @param did The DID of the document - * @param packet A DNS packet to parse into a DID Document - */ - public static async fromDnsPacket(did: string, packet: Packet): Promise { - const document: Partial = { - id: did, - }; - - const keyLookup = new Map(); - - for (const answer of packet.answers) { - if (answer.type !== 'TXT') continue; - - const dataStr = answer.data?.toString(); - // Extracts 'k' or 's' from "_k0._did" or "_s0._did" - const recordType = answer.name?.split('.')[0].substring(1, 2); - - /*eslint-disable no-case-declarations*/ - switch (recordType) { - case 'k': - const {id, t, k} = DidDht.parseTxtData(dataStr); - const keyConfigurations: { [keyType: string]: Partial } = { - '0': { - crv : 'Ed25519', - kty : 'OKP', - alg : 'EdDSA' - }, - '1': { - crv : 'secp256k1', - kty : 'EC', - alg : 'ES256K' - } - }; - const keyConfig = keyConfigurations[t]; - if (!keyConfig) { - throw new Error('Unsupported key type'); - } - - const publicKeyJwk = await Jose.keyToJwk({ - ...keyConfig, - kid : id, - keyMaterial : Encoder.decodeAsBytes(k, 'key'), - keyType : 'public' - }) as PublicKeyJwk; - - if (!document.verificationMethod) { - document.verificationMethod = []; - } - document.verificationMethod.push({ - id : `${did}#${id}`, - type : 'JsonWebKey2020', - controller : did, - publicKeyJwk : publicKeyJwk, - }); - keyLookup.set(answer.name, id); - break; - case 's': - const {id: sId, t: sType, uri} = DidDht.parseTxtData(dataStr); - - if (!document.service) { - document.service = []; - } - document.service.push({ - id : `${did}#${sId}`, - type : sType, - serviceEndpoint : uri - }); - break; - } - } - - // Extract relationships from root record - const didSuffix = did.split('did:dht:')[1]; - const potentialRootNames = ['_did', `_did.${didSuffix}`]; - - let actualRootName = null; - const root = packet.answers - .filter(answer => { - if (potentialRootNames.includes(answer.name)) { - actualRootName = answer.name; - return true; - } - return false; - }) as dns.TxtAnswer[]; - - if (root.length === 0) { - throw new Error('No root record found'); - } - - if (root.length > 1) { - throw new Error('Multiple root records found'); - } - const singleRoot = root[0] as dns.TxtAnswer; - const rootRecord = singleRoot.data?.toString().split(';'); - rootRecord?.forEach(record => { - const [type, ids] = record.split('='); - let idList = ids?.split(',').map(id => `#${keyLookup.get(`_${id}.${actualRootName}`)}`); - switch (type) { - case 'auth': - document.authentication = idList; - break; - case 'asm': - document.assertionMethod = idList; - break; - case 'agm': - document.keyAgreement = idList; - break; - case 'inv': - document.capabilityInvocation = idList; - break; - case 'del': - document.capabilityDelegation = idList; - break; - } - }); - - return document as DidDocument; - } - - private static parseTxtData(data: string): { [key: string]: string } { + * Parses TXT data from a DNS answer to extract key or service information. + * + * @param data The TXT record string data containing key-value pairs separated by commas. + * @returns An object containing parsed attributes such as 'id', 't', 'k', and 'uri'. + */ + private static parseTxtData({ data }: { data: string }): { [key: string]: string } { return data.split(',').reduce((acc, pair) => { const [key, value] = pair.split('='); acc[key] = value; return acc; }, {} as { [key: string]: string }); } -} +} \ No newline at end of file diff --git a/packages/dids/src/did-dht.ts b/packages/dids/src/did-dht.ts index cdc91d788..1bbe0252b 100644 --- a/packages/dids/src/did-dht.ts +++ b/packages/dids/src/did-dht.ts @@ -1,14 +1,21 @@ +import type { JwkKeyPair, PublicKeyJwk, Web5Crypto } from '@web5/crypto'; + import z32 from 'z32'; -import {EcdsaAlgorithm, EdDsaAlgorithm, Jose, JwkKeyPair, PublicKeyJwk, Web5Crypto} from '@web5/crypto'; -import { - DidDocument, - DidKeySetVerificationMethodKey, +import { EcdsaAlgorithm, EdDsaAlgorithm, Jose } from '@web5/crypto'; + +import type { DidMethod, + DidService, + DidDocument, + PortableDid, DidResolutionResult, - DidService, PortableDid, - VerificationRelationship + DidResolutionOptions, + VerificationRelationship, + DidKeySetVerificationMethodKey, } from './types.js'; -import {DidDht} from './dht.js'; + +import { DidDht } from './dht.js'; +import { parseDid } from './utils.js'; const SupportedCryptoKeyTypes = [ 'Ed25519', @@ -16,14 +23,14 @@ const SupportedCryptoKeyTypes = [ ] as const; export type DidDhtCreateOptions = { - publish?: boolean; - keySet?: DidDhtKeySet; - services?: DidService[]; + publish?: boolean; + keySet?: DidDhtKeySet; + services?: DidService[]; } export type DidDhtKeySet = { - identityKey?: JwkKeyPair; - verificationMethodKeys?: DidKeySetVerificationMethodKey[]; + identityKey?: JwkKeyPair; + verificationMethodKeys?: DidKeySetVerificationMethodKey[]; } export class DidDhtMethod implements DidMethod { @@ -31,19 +38,20 @@ export class DidDhtMethod implements DidMethod { public static methodName = 'dht'; /** - * Creates a new DID Document according to the did:dht spec - * @param options The options to use when creating the DID Document, including whether to publish it + * Creates a new DID Document according to the did:dht spec. + * @param options The options to use when creating the DID Document, including whether to publish it. + * @returns A promise that resolves to a PortableDid object. */ public static async create(options?: DidDhtCreateOptions): Promise { - const {publish, keySet: initialKeySet, services} = options ?? {}; + const { publish, keySet: initialKeySet, services } = options ?? {}; - // Generate missing keys if not provided in the options - const keySet = await this.generateKeySet({keySet: initialKeySet}); + // Generate missing keys, if not provided in the options. + const keySet = await this.generateKeySet({ keySet: initialKeySet }); - // Get the identifier and set it - const id = await this.getDidIdentifier({key: keySet.identityKey.publicKeyJwk}); + // Get the identifier and set it. + const id = await this.getDidIdentifier({ key: keySet.identityKey.publicKeyJwk }); - // add all other keys to the verificationMethod and relationship arrays + // Add all other keys to the verificationMethod and relationship arrays. const relationshipsMap: Partial> = {}; const verificationMethods = keySet.verificationMethodKeys.map(key => { for (const relationship of key.relationships) { @@ -62,7 +70,7 @@ export class DidDhtMethod implements DidMethod { }; }); - // add did identifier to the service ids + // Add DID identifier to the service IDs. services?.map(service => { service.id = `${id}#${service.id}`; }); @@ -74,7 +82,7 @@ export class DidDhtMethod implements DidMethod { }; if (publish) { - await this.publish(keySet, document); + await this.publish({ keySet, didDocument: document }); } return { did : document.id, @@ -83,71 +91,16 @@ export class DidDhtMethod implements DidMethod { }; } - /** - * Publishes a DID Document to the DHT - * @param keySet The key set to use to sign the DHT payload - * @param didDocument The DID Document to publish - */ - public static async publish(keySet: DidDhtKeySet, didDocument: DidDocument): Promise { - const publicCryptoKey = await Jose.jwkToCryptoKey({key: keySet.identityKey.publicKeyJwk}); - const privateCryptoKey = await Jose.jwkToCryptoKey({key: keySet.identityKey.privateKeyJwk}); - - await DidDht.publishDidDocument({ - publicKey : publicCryptoKey, - privateKey : privateCryptoKey - }, didDocument); - - return { - didDocumentMetadata : undefined, - didDocument, - didResolutionMetadata : { - contentType: 'application/json' - } - }; - } - - /** - * Resolves a DID Document from the DHT - * @param did The DID to resolve - */ - public static async resolve(did: string): Promise { - return await DidDht.getDidDocument(did); - } /** - * Gets the identifier fragment from a DID - * @param options The key to get the identifier fragment from - */ - public static async getDidIdentifier(options: { - key: PublicKeyJwk - }): Promise { - const {key} = options; - - const cryptoKey = await Jose.jwkToCryptoKey({key}); - const identifier = z32.encode(cryptoKey.material); - return 'did:dht:' + identifier; - } - - /** - * Gets the identifier fragment from a DID - * @param options The key to get the identifier fragment from - */ - public static async getDidIdentifierFragment(options: { - key: PublicKeyJwk - }): Promise { - const {key} = options; - const cryptoKey = await Jose.jwkToCryptoKey({key}); - return z32.encode(cryptoKey.material); - } - - /** - * Generates a JWK key pair - * @param options the key algorithm and key ID to use + * Generates a JWK key pair. + * @param options The key algorithm and key ID to use. + * @returns A promise that resolves to a JwkKeyPair object. */ public static async generateJwkKeyPair(options: { - keyAlgorithm: typeof SupportedCryptoKeyTypes[number], - keyId?: string - }): Promise { + keyAlgorithm: typeof SupportedCryptoKeyTypes[number], + keyId?: string + }): Promise { const {keyAlgorithm, keyId} = options; let cryptoKeyPair: Web5Crypto.CryptoKeyPair; @@ -184,7 +137,7 @@ export class DidDhtMethod implements DidMethod { jwkKeyPair.privateKeyJwk.kid = keyId; jwkKeyPair.publicKeyJwk.kid = keyId; } else { - // If a key ID is not specified, generate RFC 7638 JWK thumbprint. + // If a key ID is not specified, generate RFC 7638 JWK thumbprint. const jwkThumbprint = await Jose.jwkThumbprint({key: jwkKeyPair.publicKeyJwk}); jwkKeyPair.privateKeyJwk.kid = jwkThumbprint; jwkKeyPair.publicKeyJwk.kid = jwkThumbprint; @@ -194,12 +147,13 @@ export class DidDhtMethod implements DidMethod { } /** - * Generates a key set for a DID Document - * @param options The key set to use when generating the key set + * Generates a key set for a DID Document. + * @param options The key set to use when generating the key set. + * @returns A promise that resolves to a DidDhtKeySet object. */ public static async generateKeySet(options?: { - keySet?: DidDhtKeySet - }): Promise { + keySet?: DidDhtKeySet + }): Promise { let {keySet = {}} = options ?? {}; if (!keySet.identityKey) { @@ -235,4 +189,115 @@ export class DidDhtMethod implements DidMethod { return keySet; } -} + + /** + * Gets the identifier fragment from a DID. + * @param options The key to get the identifier fragment from. + * @returns A promise that resolves to a string containing the identifier. + */ + public static async getDidIdentifier(options: { + key: PublicKeyJwk + }): Promise { + const {key} = options; + + const cryptoKey = await Jose.jwkToCryptoKey({key}); + const identifier = z32.encode(cryptoKey.material); + return 'did:dht:' + identifier; + } + + /** + * Gets the identifier fragment from a DID. + * @param options The key to get the identifier fragment from. + * @returns A promise that resolves to a string containing the identifier fragment. + */ + public static async getDidIdentifierFragment(options: { + key: PublicKeyJwk + }): Promise { + const {key} = options; + const cryptoKey = await Jose.jwkToCryptoKey({key}); + return z32.encode(cryptoKey.material); + } + + /** + * Publishes a DID Document to the DHT. + * @param keySet The key set to use to sign the DHT payload. + * @param didDocument The DID Document to publish. + * @returns A boolean indicating the success of the publishing operation. + */ + public static async publish({ didDocument, keySet }: { + didDocument: DidDocument, + keySet: DidDhtKeySet + }): Promise { + const publicCryptoKey = await Jose.jwkToCryptoKey({key: keySet.identityKey.publicKeyJwk}); + const privateCryptoKey = await Jose.jwkToCryptoKey({key: keySet.identityKey.privateKeyJwk}); + + const isPublished = await DidDht.publishDidDocument({ + keyPair: { + publicKey : publicCryptoKey, + privateKey : privateCryptoKey + }, + didDocument + }); + + return isPublished; + } + + /** + * Resolves a DID Document based on the specified options. + * + * @param options - Configuration for resolving a DID Document. + * @param options.didUrl - The DID URL to resolve. + * @param options.resolutionOptions - Optional settings for the DID resolution process as defined in the DID Core specification. + * @returns A Promise that resolves to a `DidResolutionResult`, containing the resolved DID Document and associated metadata. + */ + public static async resolve(options: { + didUrl: string, + resolutionOptions?: DidResolutionOptions + }): Promise { + const { didUrl, resolutionOptions: _ } = options; + // TODO: Implement resolutionOptions as defined in https://www.w3.org/TR/did-core/#did-resolution + + const parsedDid = parseDid({ didUrl }); + if (!parsedDid) { + return { + '@context' : 'https://w3id.org/did-resolution/v1', + didDocument : undefined, + didDocumentMetadata : {}, + didResolutionMetadata : { + contentType : 'application/did+ld+json', + error : 'invalidDid', + errorMessage : `Cannot parse DID: ${didUrl}` + } + }; + } + + if (parsedDid.method !== 'dht') { + return { + '@context' : 'https://w3id.org/did-resolution/v1', + didDocument : undefined, + didDocumentMetadata : {}, + didResolutionMetadata : { + contentType : 'application/did+ld+json', + error : 'methodNotSupported', + errorMessage : `Method not supported: ${parsedDid.method}` + } + }; + } + + const didDocument = await DidDht.getDidDocument({ did: parsedDid.did }); + + return { + '@context' : 'https://w3id.org/did-resolution/v1', + didDocument, + didDocumentMetadata : {}, + didResolutionMetadata : { + contentType : 'application/did+ld+json', + did : { + didString : parsedDid.did, + methodSpecificId : parsedDid.id, + method : parsedDid.method + } + } + }; + } +} \ No newline at end of file diff --git a/packages/dids/src/did-key.ts b/packages/dids/src/did-key.ts index 886ea6fdf..8964122e7 100644 --- a/packages/dids/src/did-key.ts +++ b/packages/dids/src/did-key.ts @@ -1,7 +1,7 @@ import type { PrivateKeyJwk, PublicKeyJwk, Web5Crypto } from '@web5/crypto'; import { universalTypeOf } from '@web5/common'; -import { keyToMultibaseId, multibaseIdToKey } from '@web5/crypto/utils'; +import { keyToMultibaseId, multibaseIdToKey } from '@web5/crypto/utils'; import { Jose, Ed25519, @@ -10,7 +10,14 @@ import { EdDsaAlgorithm, } from '@web5/crypto'; -import type { DidDocument, DidMethod, DidResolutionOptions, DidResolutionResult, PortableDid, VerificationMethod } from './types.js'; +import type { + DidMethod, + DidDocument, + PortableDid, + VerificationMethod, + DidResolutionResult, + DidResolutionOptions, +} from './types.js'; import { DidKeySetVerificationMethodKey } from './types.js'; import { getVerificationMethodTypes, parseDid } from './utils.js'; diff --git a/packages/dids/tests/dht.spec.ts b/packages/dids/tests/dht.spec.ts index ce84e8d64..125a30a0a 100644 --- a/packages/dids/tests/dht.spec.ts +++ b/packages/dids/tests/dht.spec.ts @@ -1,24 +1,27 @@ -import {expect} from 'chai'; -import {Jose} from '@web5/crypto'; -import {DidDht} from '../src/dht.js'; -import {DidDhtKeySet, DidDhtMethod} from '../src/did-dht.js'; -import {DidKeySetVerificationMethodKey, DidService} from '../src/index.js'; +import { expect } from 'chai'; +import { Jose } from '@web5/crypto'; +import type { DidDhtKeySet } from '../src/did-dht.js'; +import type { DidKeySetVerificationMethodKey, DidService } from '../src/types.js'; -describe('DHT', function () { - this.timeout('15000'); // 15 seconds +import { DidDht } from '../src/dht.js'; +import { DidDhtMethod } from '../src/did-dht.js'; +describe.only('DidDht', () => { it('should create a put and parse a get request', async () => { - const {document, keySet} = await DidDhtMethod.create(); + const { document, keySet } = await DidDhtMethod.create(); const ks = keySet as DidDhtKeySet; - const publicCryptoKey = await Jose.jwkToCryptoKey({key: ks.identityKey.publicKeyJwk}); - const privateCryptoKey = await Jose.jwkToCryptoKey({key: ks.identityKey.privateKeyJwk}); + const publicCryptoKey = await Jose.jwkToCryptoKey({ key: ks.identityKey.publicKeyJwk }); + const privateCryptoKey = await Jose.jwkToCryptoKey({ key: ks.identityKey.privateKeyJwk }); const published = await DidDht.publishDidDocument({ - publicKey : publicCryptoKey, - privateKey : privateCryptoKey - }, document); + keyPair: { + publicKey : publicCryptoKey, + privateKey : privateCryptoKey + }, + didDocument: document + }); expect(published).to.be.true; @@ -26,7 +29,7 @@ describe('DHT', function () { const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); await wait(1000*10); - const gotDid = await DidDht.getDidDocument(document.id); + const gotDid = await DidDht.getDidDocument({ did: document.id }); expect(gotDid.id).to.deep.equal(document.id); expect(gotDid.capabilityDelegation).to.deep.equal(document.capabilityDelegation); expect(gotDid.capabilityInvocation).to.deep.equal(document.capabilityInvocation); @@ -38,44 +41,44 @@ describe('DHT', function () { expect(gotDid.verificationMethod[0].controller).to.deep.equal(document.verificationMethod[0].controller); expect(gotDid.verificationMethod[0].publicKeyJwk.kid).to.deep.equal(document.verificationMethod[0].publicKeyJwk.kid); expect(gotDid.verificationMethod[0].publicKeyJwk.kty).to.deep.equal(document.verificationMethod[0].publicKeyJwk.kty); - }); -}); + }).timeout(15000); // 15 seconds -describe('Codec', async () => { - it('encodes and decodes a DID Document as a DNS Packet', async () => { - const services: DidService[] = [{ - id : 'dwn', - type : 'DecentralizedWebNode', - serviceEndpoint : 'https://example.com/dwn' - }]; - const secp = await DidDhtMethod.generateJwkKeyPair({keyAlgorithm: 'secp256k1'}); - const vm: DidKeySetVerificationMethodKey = { - publicKeyJwk : secp.publicKeyJwk, - privateKeyJwk : secp.privateKeyJwk, - relationships : ['authentication', 'assertionMethod'] - }; - const keySet = { - verificationMethodKeys: [vm], - }; - const {did, document} = await DidDhtMethod.create({services: services, keySet: keySet}); - const encoded = await DidDht.toDnsPacket(document); - const decoded = await DidDht.fromDnsPacket(did, encoded); + describe('Codec', async () => { + it('encodes and decodes a DID Document as a DNS Packet', async () => { + const services: DidService[] = [{ + id : 'dwn', + type : 'DecentralizedWebNode', + serviceEndpoint : 'https://example.com/dwn' + }]; + const secp = await DidDhtMethod.generateJwkKeyPair({keyAlgorithm: 'secp256k1'}); + const vm: DidKeySetVerificationMethodKey = { + publicKeyJwk : secp.publicKeyJwk, + privateKeyJwk : secp.privateKeyJwk, + relationships : ['authentication', 'assertionMethod'] + }; + const keySet = { + verificationMethodKeys: [vm], + }; + const { did, document } = await DidDhtMethod.create({ services: services, keySet: keySet }); + const encoded = await DidDht.toDnsPacket({ didDocument: document }); + const decoded = await DidDht.fromDnsPacket({ did, packet: encoded }); - expect(document.id).to.deep.equal(decoded.id); - expect(document.capabilityDelegation).to.deep.equal(decoded.capabilityDelegation); - expect(document.capabilityInvocation).to.deep.equal(decoded.capabilityInvocation); - expect(document.keyAgreement).to.deep.equal(decoded.keyAgreement); - expect(document.service).to.deep.equal(decoded.service); - expect(document.verificationMethod.length).to.deep.equal(decoded.verificationMethod.length); - expect(document.verificationMethod[0].id).to.deep.equal(decoded.verificationMethod[0].id); - expect(document.verificationMethod[0].type).to.deep.equal(decoded.verificationMethod[0].type); - expect(document.verificationMethod[0].controller).to.deep.equal(decoded.verificationMethod[0].controller); - expect(document.verificationMethod[0].publicKeyJwk.kid).to.deep.equal(decoded.verificationMethod[0].publicKeyJwk.kid); - expect(document.verificationMethod[0].publicKeyJwk.kty).to.deep.equal(decoded.verificationMethod[0].publicKeyJwk.kty); - expect(document.verificationMethod[1].id).to.deep.equal(decoded.verificationMethod[1].id); - expect(document.verificationMethod[1].type).to.deep.equal(decoded.verificationMethod[1].type); - expect(document.verificationMethod[1].controller).to.deep.equal(decoded.verificationMethod[1].controller); - expect(document.verificationMethod[1].publicKeyJwk.kid).to.deep.equal(decoded.verificationMethod[1].publicKeyJwk.kid); - expect(document.verificationMethod[1].publicKeyJwk.kty).to.deep.equal(decoded.verificationMethod[1].publicKeyJwk.kty); + expect(document.id).to.deep.equal(decoded.id); + expect(document.capabilityDelegation).to.deep.equal(decoded.capabilityDelegation); + expect(document.capabilityInvocation).to.deep.equal(decoded.capabilityInvocation); + expect(document.keyAgreement).to.deep.equal(decoded.keyAgreement); + expect(document.service).to.deep.equal(decoded.service); + expect(document.verificationMethod.length).to.deep.equal(decoded.verificationMethod.length); + expect(document.verificationMethod[0].id).to.deep.equal(decoded.verificationMethod[0].id); + expect(document.verificationMethod[0].type).to.deep.equal(decoded.verificationMethod[0].type); + expect(document.verificationMethod[0].controller).to.deep.equal(decoded.verificationMethod[0].controller); + expect(document.verificationMethod[0].publicKeyJwk.kid).to.deep.equal(decoded.verificationMethod[0].publicKeyJwk.kid); + expect(document.verificationMethod[0].publicKeyJwk.kty).to.deep.equal(decoded.verificationMethod[0].publicKeyJwk.kty); + expect(document.verificationMethod[1].id).to.deep.equal(decoded.verificationMethod[1].id); + expect(document.verificationMethod[1].type).to.deep.equal(decoded.verificationMethod[1].type); + expect(document.verificationMethod[1].controller).to.deep.equal(decoded.verificationMethod[1].controller); + expect(document.verificationMethod[1].publicKeyJwk.kid).to.deep.equal(decoded.verificationMethod[1].publicKeyJwk.kid); + expect(document.verificationMethod[1].publicKeyJwk.kty).to.deep.equal(decoded.verificationMethod[1].publicKeyJwk.kty); + }); }); -}); +}); \ No newline at end of file diff --git a/packages/dids/tests/did-dht.spec.ts b/packages/dids/tests/did-dht.spec.ts index cf85cad83..3504b70f0 100644 --- a/packages/dids/tests/did-dht.spec.ts +++ b/packages/dids/tests/did-dht.spec.ts @@ -1,17 +1,19 @@ -import chai from 'chai'; -import {expect} from 'chai'; +import chai, { expect } from 'chai'; import chaiAsPromised from 'chai-as-promised'; -import {DidDhtKeySet, DidDhtMethod} from '../src/did-dht.js'; -import {DidKeySetVerificationMethodKey, DidService} from '../src/index.js'; + +import type { DidDhtKeySet } from '../src/did-dht.js'; +import type { DidKeySetVerificationMethodKey, DidService } from '../src/types.js'; + +import { DidDhtMethod } from '../src/did-dht.js'; chai.use(chaiAsPromised); const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); -describe('did-dht', () => { +describe.only('DidDhtMethod', () => { describe('keypairs', () => { - it('should generate a keypair', async () => { - const ed25519KeyPair = await DidDhtMethod.generateJwkKeyPair({keyAlgorithm: 'Ed25519'}); + it('should generate a key pair', async () => { + const ed25519KeyPair = await DidDhtMethod.generateJwkKeyPair({ keyAlgorithm: 'Ed25519' }); expect(ed25519KeyPair).to.exist; expect(ed25519KeyPair).to.have.property('privateKeyJwk'); @@ -20,7 +22,7 @@ describe('did-dht', () => { expect(ed25519KeyPair.publicKeyJwk.alg).to.equal('EdDSA'); expect(ed25519KeyPair.publicKeyJwk.kty).to.equal('OKP'); - const secp256k1KeyPair = await DidDhtMethod.generateJwkKeyPair({keyAlgorithm: 'secp256k1'}); + const secp256k1KeyPair = await DidDhtMethod.generateJwkKeyPair({ keyAlgorithm: 'secp256k1' }); expect(secp256k1KeyPair).to.exist; expect(secp256k1KeyPair).to.have.property('privateKeyJwk'); @@ -47,14 +49,16 @@ describe('did-dht', () => { }); it('should generate a keyset with an identity keyset passed in (wrong kid)', async () => { - const ed25519KeyPair = await DidDhtMethod.generateJwkKeyPair({keyAlgorithm: 'Ed25519'}); + const ed25519KeyPair = await DidDhtMethod.generateJwkKeyPair({ keyAlgorithm: 'Ed25519' }); - expect(DidDhtMethod.generateKeySet({keySet: {identityKey: ed25519KeyPair}})).to.be.rejectedWith('The identity key must have a kid of 0'); + expect(DidDhtMethod.generateKeySet({ + keySet: { identityKey: ed25519KeyPair } + })).to.be.rejectedWith('The identity key must have a kid of 0'); }); it('should generate a keyset with an identity keyset passed in (correct kid)', async () => { - const ed25519KeyPair = await DidDhtMethod.generateJwkKeyPair({keyId: '0', keyAlgorithm: 'Ed25519'}); - const keySet = await DidDhtMethod.generateKeySet({keySet: {identityKey: ed25519KeyPair}}); + const ed25519KeyPair = await DidDhtMethod.generateJwkKeyPair({ keyId: '0', keyAlgorithm: 'Ed25519' }); + const keySet = await DidDhtMethod.generateKeySet({ keySet: { identityKey: ed25519KeyPair } }); expect(keySet).to.exist; expect(keySet).to.have.property('identityKey'); @@ -67,14 +71,14 @@ describe('did-dht', () => { }); it('should generate a keyset with a non identity keyset passed in', async () => { - const ed25519KeyPair = await DidDhtMethod.generateJwkKeyPair({keyAlgorithm: 'Ed25519'}); + const ed25519KeyPair = await DidDhtMethod.generateJwkKeyPair({ keyAlgorithm: 'Ed25519' }); const vm: DidKeySetVerificationMethodKey = { publicKeyJwk : ed25519KeyPair.publicKeyJwk, privateKeyJwk : ed25519KeyPair.privateKeyJwk, relationships : ['authentication', 'assertionMethod', 'capabilityInvocation', 'capabilityDelegation'] }; - const keySet = await DidDhtMethod.generateKeySet({keySet: {verificationMethodKeys: [vm]}}); + const keySet = await DidDhtMethod.generateKeySet({ keySet: { verificationMethodKeys: [vm] } }); expect(keySet).to.exist; expect(keySet).to.have.property('identityKey'); @@ -94,8 +98,8 @@ describe('did-dht', () => { describe('dids', () => { it('should generate a did identifier given a public key jwk', async () => { - const ed25519KeyPair = await DidDhtMethod.generateJwkKeyPair({keyAlgorithm: 'Ed25519'}); - const did = await DidDhtMethod.getDidIdentifier({key: ed25519KeyPair.publicKeyJwk}); + const ed25519KeyPair = await DidDhtMethod.generateJwkKeyPair({ keyAlgorithm: 'Ed25519' }); + const did = await DidDhtMethod.getDidIdentifier({ key: ed25519KeyPair.publicKeyJwk }); expect(did).to.exist; expect(did).to.contain('did:dht:'); @@ -131,14 +135,14 @@ describe('did-dht', () => { }); it('should create a did document with a non identity key option', async () => { - const ed25519KeyPair = await DidDhtMethod.generateJwkKeyPair({keyAlgorithm: 'Ed25519'}); + const ed25519KeyPair = await DidDhtMethod.generateJwkKeyPair({ keyAlgorithm: 'Ed25519' }); const vm: DidKeySetVerificationMethodKey = { publicKeyJwk : ed25519KeyPair.publicKeyJwk, privateKeyJwk : ed25519KeyPair.privateKeyJwk, relationships : ['authentication', 'assertionMethod', 'capabilityInvocation', 'capabilityDelegation'] }; - const keySet = await DidDhtMethod.generateKeySet({keySet: {verificationMethodKeys: [vm]}}); + const keySet = await DidDhtMethod.generateKeySet({ keySet: { verificationMethodKeys: [vm] }}); const {document} = await DidDhtMethod.create({keySet}); expect(document).to.exist; @@ -172,7 +176,7 @@ describe('did-dht', () => { type : 'agent', serviceEndpoint : 'https://example.com/agent' }]; - const {document} = await DidDhtMethod.create({services}); + const {document} = await DidDhtMethod.create({ services }); expect(document).to.exist; expect(document.id).to.contain('did:dht:'); @@ -200,44 +204,39 @@ describe('did-dht', () => { this.timeout(20000); // 20 seconds it('should publish and get a did document', async () => { - const {document, keySet} = await DidDhtMethod.create(); - const didResolutionResult = await DidDhtMethod.publish(keySet, document); - - expect(didResolutionResult).to.exist; - expect(didResolutionResult.didDocument).to.exist; - expect(didResolutionResult.didDocument.id).to.equal(document.id); - expect(didResolutionResult.didDocument.verificationMethod).to.exist; - expect(didResolutionResult.didDocument.verificationMethod).to.have.lengthOf(1); - expect(didResolutionResult.didDocument.verificationMethod[0].id).to.equal(`${document.id}#0`); - expect(didResolutionResult.didDocument.verificationMethod[0].publicKeyJwk).to.exist; - expect(didResolutionResult.didDocument.verificationMethod[0].publicKeyJwk.kid).to.equal('0'); - expect(didResolutionResult.didDocument.service).to.not.exist; + const { document, keySet } = await DidDhtMethod.create(); + const isPublished = await DidDhtMethod.publish({ keySet, didDocument: document }); + + expect(isPublished).to.be.true; // wait for propagation await wait(1000*10); - const gotDid = await DidDhtMethod.resolve(document.id); - expect(gotDid.id).to.deep.equal(document.id); - expect(gotDid.service).to.deep.equal(document.service); - expect(gotDid.verificationMethod[0].id).to.deep.equal(document.verificationMethod[0].id); - expect(gotDid.verificationMethod[0].type).to.deep.equal(document.verificationMethod[0].type); - expect(gotDid.verificationMethod[0].controller).to.deep.equal(document.verificationMethod[0].controller); - expect(gotDid.verificationMethod[0].publicKeyJwk.kid).to.deep.equal(document.verificationMethod[0].publicKeyJwk.kid); + const didResolutionResult = await DidDhtMethod.resolve({ didUrl: document.id }); + const didDocument = didResolutionResult.didDocument; + expect(didDocument.id).to.deep.equal(document.id); + expect(didDocument.service).to.deep.equal(document.service); + expect(didDocument.verificationMethod[0].id).to.deep.equal(document.verificationMethod[0].id); + expect(didDocument.verificationMethod[0].type).to.deep.equal(document.verificationMethod[0].type); + expect(didDocument.verificationMethod[0].controller).to.deep.equal(document.verificationMethod[0].controller); + expect(didDocument.verificationMethod[0].publicKeyJwk.kid).to.deep.equal(document.verificationMethod[0].publicKeyJwk.kid); }); it('should create with publish and get a did document', async () => { - const {document} = await DidDhtMethod.create({publish: true}); + const { document } = await DidDhtMethod.create({ publish: true }); + const did = document.id; // wait for propagation await wait(1000*10); - const gotDid = await DidDhtMethod.resolve(document.id); - expect(gotDid.id).to.deep.equal(document.id); - expect(gotDid.service).to.deep.equal(document.service); - expect(gotDid.verificationMethod[0].id).to.deep.equal(document.verificationMethod[0].id); - expect(gotDid.verificationMethod[0].type).to.deep.equal(document.verificationMethod[0].type); - expect(gotDid.verificationMethod[0].controller).to.deep.equal(document.verificationMethod[0].controller); - expect(gotDid.verificationMethod[0].publicKeyJwk.kid).to.deep.equal(document.verificationMethod[0].publicKeyJwk.kid); + const didResolutionResult = await DidDhtMethod.resolve({ didUrl: did }); + const resolvedDocument = didResolutionResult.didDocument; + expect(resolvedDocument.id).to.deep.equal(document.id); + expect(resolvedDocument.service).to.deep.equal(document.service); + expect(resolvedDocument.verificationMethod[0].id).to.deep.equal(document.verificationMethod[0].id); + expect(resolvedDocument.verificationMethod[0].type).to.deep.equal(document.verificationMethod[0].type); + expect(resolvedDocument.verificationMethod[0].controller).to.deep.equal(document.verificationMethod[0].controller); + expect(resolvedDocument.verificationMethod[0].publicKeyJwk.kid).to.deep.equal(document.verificationMethod[0].publicKeyJwk.kid); }); }); });