From b2cbe7bb6cb8e22f61b1638fd990c00e8cb7292d Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Fri, 16 Feb 2024 04:11:37 -0500 Subject: [PATCH] Fix handling of custom service properties (#413) * Fix handling of custom service properties --------- Signed-off-by: Frank Hinek --- packages/dids/src/methods/did-dht.ts | 22 +- packages/dids/tests/methods/did-dht.spec.ts | 350 +++++++++++++++++++- 2 files changed, 356 insertions(+), 16 deletions(-) diff --git a/packages/dids/src/methods/did-dht.ts b/packages/dids/src/methods/did-dht.ts index 7a3d574a0..cadb92955 100644 --- a/packages/dids/src/methods/did-dht.ts +++ b/packages/dids/src/methods/did-dht.ts @@ -966,7 +966,7 @@ export class DidDhtDocument { * @returns A Promise resolving to a {@link DidResolutionResult} object containing the DID * document and its metadata. */ - private static async fromDnsPacket({ didUri, dnsPacket }: { + public static async fromDnsPacket({ didUri, dnsPacket }: { didUri: string; dnsPacket: Packet; }): Promise { @@ -1055,11 +1055,16 @@ export class DidDhtDocument { // The service endpoint can either be a string or an array of strings. const serviceEndpoint = se.includes(VALUE_SEPARATOR) ? se.split(VALUE_SEPARATOR) : se; + // Convert custom property values to either a string or an array of strings. + const serviceProperties = Object.fromEntries(Object.entries(customProperties).map( + ([k, v]) => [k, v.includes(VALUE_SEPARATOR) ? v.split(VALUE_SEPARATOR) : v] + )); + // Initialize the `service` array if it does not already exist. didDocument.service ??= []; didDocument.service.push({ - ...customProperties, + ...serviceProperties, id : `${didUri}#${id}`, type : t, serviceEndpoint @@ -1115,7 +1120,7 @@ export class DidDhtDocument { * @param params.didMetadata - The DID metadata to include in the DNS packet. * @returns A promise that resolves to a DNS packet. */ - private static async toDnsPacket({ didDocument, didMetadata }: { + public static async toDnsPacket({ didDocument, didMetadata }: { didDocument: DidDocument; didMetadata: DidMetadata; }): Promise { @@ -1188,13 +1193,14 @@ export class DidDhtDocument { didDocument.service?.forEach((service, index) => { const dnsRecordId = `s${index}`; serviceIds.push(dnsRecordId); - const serviceId = service.id.split('#').pop()!; // Remove fragment prefix, if any. - const serviceEndpoint = Array.isArray(service.serviceEndpoint) - ? service.serviceEndpoint.join(',') - : service.serviceEndpoint; + let { id, type: t, serviceEndpoint: se, ...customProperties } = service; + id = extractDidFragment(id)!; + se = Array.isArray(se) ? se.join(',') : se; // Define the data for the DNS TXT record. - const txtData = [`id=${serviceId}`, `t=${service.type}`, `se=${serviceEndpoint}`]; + const txtData = Object.entries({ id, t, se, ...customProperties }).map( + ([key, value]) => `${key}=${value}` + ); // Add a TXT record for the verification method. dnsAnswerRecords.push({ diff --git a/packages/dids/tests/methods/did-dht.spec.ts b/packages/dids/tests/methods/did-dht.spec.ts index 2e17182e8..916ae475f 100644 --- a/packages/dids/tests/methods/did-dht.spec.ts +++ b/packages/dids/tests/methods/did-dht.spec.ts @@ -5,9 +5,7 @@ import { Convert } from '@web5/common'; import type { PortableDid } from '../../src/types/portable-did.js'; import { DidErrorCode } from '../../src/did-error.js'; -import { DidDht, DidDhtRegisteredDidType } from '../../src/methods/did-dht.js'; -import DidDhtResolveTestVector from '../../../../web5-spec/test-vectors/did_dht/resolve.json' assert { type: 'json' }; - +import { DidDht, DidDhtDocument, DidDhtRegisteredDidType } from '../../src/methods/did-dht.js'; // Helper function to create a mocked fetch response that fails and returns a 404 Not Found. const fetchNotFoundResponse = () => ({ @@ -804,13 +802,349 @@ describe('DidDht', () => { expect(didResolutionResult.didResolutionMetadata).to.have.property('error', 'invalidDidDocumentLength'); }); }); +}); +describe('DidDhtDocument', () => { + describe('fromDnsPacket()', async () => { + it('handles custom string properties for services', async () => { + const didUri = 'did:dht:hpmp9uur565nkimpwdzom7ehbuabnsba658xwwynyk7awcd15bko'; + + const didResolutionResult = await DidDhtDocument.fromDnsPacket({ + didUri, + dnsPacket: { + id : 0, + type : 'response', + flags : 1024, + flag_qr : true, + opcode : 'QUERY', + flag_aa : true, + flag_tc : false, + flag_rd : false, + flag_ra : false, + flag_z : false, + flag_ad : false, + flag_cd : false, + rcode : 'NOERROR', + questions : [ + ], + answers: [ + { + name : '_k0._did.hpmp9uur565nkimpwdzom7ehbuabnsba658xwwynyk7awcd15bko', + type : 'TXT', + ttl : 7200, + class : 'IN', + data : [ + new Uint8Array([105, 100, 61, 48, 59, 116, 61, 48, 59, 107, 61, 52, 49, 98, 102, 122, 109, 84, 102, 116, 105, 86, 86, 98, 97, 68, 118, 66, 102, 85, 99, 68, 80, 65, 82, 87, 68, 106, 50, 122, 118, 112, 81, 65, 103, 75, 55, 105, 106, 66, 121, 50, 70, 85]), + ], + }, + { + name : '_k1._did.hpmp9uur565nkimpwdzom7ehbuabnsba658xwwynyk7awcd15bko', + type : 'TXT', + ttl : 7200, + class : 'IN', + data : [ + new Uint8Array([105, 100, 61, 115, 105, 103, 59, 116, 61, 48, 59, 107, 61, 73, 120, 57, 114, 84, 52, 52, 81, 75, 110, 73, 106, 78, 101, 66, 53, 49, 45, 79, 82, 108, 119, 111, 67, 98, 76, 75, 114, 45, 104, 115, 79, 89, 103, 108, 52, 103, 78, 57, 84, 122, 73, 85]), + ], + }, + { + name : '_k2._did.hpmp9uur565nkimpwdzom7ehbuabnsba658xwwynyk7awcd15bko', + type : 'TXT', + ttl : 7200, + class : 'IN', + data : [ + new Uint8Array([105, 100, 61, 101, 110, 99, 59, 116, 61, 49, 59, 107, 61, 66, 71, 65, 105, 105, 83, 48, 118, 78, 110, 111, 101, 57, 76, 57, 108, 99, 103, 101, 116, 54, 122, 97, 108, 68, 68, 106, 56, 90, 120, 66, 76, 119, 90, 86, 73, 97, 56, 72, 119, 122, 106, 117, 112, 107, 65, 55, 54, 108, 78, 74, 52, 105, 49, 57, 48, 117, 74, 86, 101, 108, 81, 106, 90, 57, 116, 120, 89, 98, 85, 85, 56, 112, 121, 107, 51, 97, 120, 103, 72, 120, 121, 68, 82, 86, 72, 56]), + ], + }, + { + name : '_s0._did.hpmp9uur565nkimpwdzom7ehbuabnsba658xwwynyk7awcd15bko', + type : 'TXT', + ttl : 7200, + class : 'IN', + data : [ + new Uint8Array([105, 100, 61, 100, 119, 110, 59, 116, 61, 68, 101, 99, 101, 110, 116, 114, 97, 108, 105, 122, 101, 100, 87, 101, 98, 78, 111, 100, 101, 59, 115, 101, 61, 104, 116, 116, 112, 115, 58, 47, 47, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, 47, 100, 119, 110, 50, 59, 101, 110, 99, 61, 35, 101, 110, 99, 59, 115, 105, 103, 61, 35, 115, 105, 103]), + ], + }, + { + name : '_did.hpmp9uur565nkimpwdzom7ehbuabnsba658xwwynyk7awcd15bko', + type : 'TXT', + ttl : 7200, + class : 'IN', + data : [ + new Uint8Array([118, 61, 48, 59, 118, 109, 61, 107, 48, 44, 107, 49, 44, 107, 50, 59, 97, 117, 116, 104, 61, 107, 48, 44, 107, 49, 59, 97, 115, 109, 61, 107, 48, 44, 107, 49, 59, 97, 103, 109, 61, 107, 50, 59, 100, 101, 108, 61, 107, 48, 59, 105, 110, 118, 61, 107, 48, 59, 115, 118, 99, 61, 115, 48]), + ], + }, + ], + authorities: [ + ], + additionals: [ + ], + } + }); - describe('Web5TestVectorsDidDht', () => { - it('resolve', async () => { - for (const vector of DidDhtResolveTestVector.vectors) { - const didResolutionResult = await DidDht.resolve(vector.input.didUri); - expect(didResolutionResult.didResolutionMetadata.error).to.equal(vector.output.didResolutionMetadata.error); + expect(didResolutionResult).to.have.property('didDocument'); + expect(didResolutionResult).to.have.property('didDocumentMetadata'); + expect(didResolutionResult).to.have.property('didResolutionMetadata'); + + expect(didResolutionResult.didDocument).to.have.property('id', didUri); + expect(didResolutionResult.didDocument!.service![0].sig).to.equal('#sig'); + expect(didResolutionResult.didDocument!.service![0].enc).to.equal('#enc'); + }); + + it('handles custom array for services', async () => { + const didUri = 'did:dht:1ati6y645tprnm9nkqgguj718bmtqw5mbjq87ttbrntfokekzeso'; + + const didResolutionResult = await DidDhtDocument.fromDnsPacket({ + didUri, + dnsPacket: { + id : 0, + type : 'response', + flags : 1024, + flag_qr : true, + opcode : 'QUERY', + flag_aa : true, + flag_tc : false, + flag_rd : false, + flag_ra : false, + flag_z : false, + flag_ad : false, + flag_cd : false, + rcode : 'NOERROR', + questions : [ + ], + answers: [ + { + name : '_k0._did.1ati6y645tprnm9nkqgguj718bmtqw5mbjq87ttbrntfokekzeso', + type : 'TXT', + ttl : 7200, + class : 'IN', + data : [ + new Uint8Array([105, 100, 61, 48, 59, 116, 61, 48, 59, 107, 61, 108, 105, 78, 102, 65, 57, 114, 99, 87, 107, 69, 118, 52, 108, 79, 77, 97, 97, 101, 121, 79, 70, 99, 88, 85, 50, 115, 75, 88, 72, 55, 71, 73, 83, 67, 105, 87, 67, 107, 75, 117, 105, 48]), + ], + }, + { + name : '_k1._did.1ati6y645tprnm9nkqgguj718bmtqw5mbjq87ttbrntfokekzeso', + type : 'TXT', + ttl : 7200, + class : 'IN', + data : [ + new Uint8Array([105, 100, 61, 97, 117, 116, 104, 59, 116, 61, 48, 59, 107, 61, 97, 103, 66, 54, 55, 109, 113, 70, 88, 76, 45, 102, 79, 115, 95, 119, 122, 100, 74, 65, 85, 104, 86, 83, 53, 120, 90, 112, 70, 56, 50, 55, 55, 72, 88, 103, 86, 110, 121, 78, 103, 95, 99]), + ], + }, + { + name : '_k2._did.1ati6y645tprnm9nkqgguj718bmtqw5mbjq87ttbrntfokekzeso', + type : 'TXT', + ttl : 7200, + class : 'IN', + data : [ + new Uint8Array([105, 100, 61, 97, 115, 115, 101, 114, 116, 59, 116, 61, 48, 59, 107, 61, 54, 121, 109, 110, 97, 118, 116, 77, 114, 98, 85, 71, 84, 65, 86, 45, 108, 86, 87, 108, 73, 115, 116, 73, 66, 121, 110, 86, 75, 50, 103, 114, 73, 95, 113, 68, 104, 109, 72, 100, 83, 113, 115]), + ], + }, + { + name : '_s0._did.1ati6y645tprnm9nkqgguj718bmtqw5mbjq87ttbrntfokekzeso', + type : 'TXT', + ttl : 7200, + class : 'IN', + data : [ + new Uint8Array([105, 100, 61, 100, 119, 110, 59, 116, 61, 68, 101, 99, 101, 110, 116, 114, 97, 108, 105, 122, 101, 100, 87, 101, 98, 78, 111, 100, 101, 59, 115, 101, 61, 104, 116, 116, 112, 115, 58, 47, 47, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, 47, 100, 119, 110, 59, 115, 105, 103, 61, 35, 97, 117, 116, 104, 44, 35, 97, 115, 115, 101, 114, 116]), + ], + }, + { + name : '_did.1ati6y645tprnm9nkqgguj718bmtqw5mbjq87ttbrntfokekzeso', + type : 'TXT', + ttl : 7200, + class : 'IN', + data : [ + new Uint8Array([118, 61, 48, 59, 118, 109, 61, 107, 48, 44, 107, 49, 44, 107, 50, 59, 97, 117, 116, 104, 61, 107, 48, 44, 107, 49, 59, 97, 115, 109, 61, 107, 48, 44, 107, 50, 59, 100, 101, 108, 61, 107, 48, 59, 105, 110, 118, 61, 107, 48, 59, 115, 118, 99, 61, 115, 48]), + ], + }, + ], + authorities: [ + ], + additionals: [ + ], + } + }); + + expect(didResolutionResult).to.have.property('didDocument'); + expect(didResolutionResult).to.have.property('didDocumentMetadata'); + expect(didResolutionResult).to.have.property('didResolutionMetadata'); + + expect(didResolutionResult.didDocument).to.have.property('id', didUri); + expect(didResolutionResult.didDocument!.service![0].sig).to.have.length(2); + expect(didResolutionResult.didDocument!.service![0].sig).to.deep.equal(['#auth', '#assert']); + }); + }); + + describe('toDnsPacket()', () => { + it('handles custom string properties for services', async () => { + const dnsPacket = await DidDhtDocument.toDnsPacket({ + didDocument: { + id : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery', + verificationMethod : [ + { + id : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#0', + type : 'JsonWebKey', + controller : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery', + publicKeyJwk : { + crv : 'Ed25519', + kty : 'OKP', + x : '2zHGF5m_DhcPbBZB6ooIxIOR-Vw-yJVYSPo2NgCMkgg', + kid : 'KDT9PKj4_z7gPk2s279Y-OGlMtt_L93oJzIaiVrrySU', + alg : 'EdDSA', + }, + }, + { + id : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#sig', + type : 'JsonWebKey', + controller : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery', + publicKeyJwk : { + crv : 'Ed25519', + kty : 'OKP', + x : 'FrrBhqvAWxE4lstj-IWgN8_5-O4L1KuZjdNjn5bX_dw', + kid : 'dRnxo2XQ7QT1is5WmpEefwEz3z4_4JdpGea6KWUn3ww', + alg : 'EdDSA', + }, + }, + { + id : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#enc', + type : 'JsonWebKey', + controller : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery', + publicKeyJwk : { + kty : 'EC', + crv : 'secp256k1', + x : 'e1_pCWZwI9cxdrotVKIT8t75itk22XkpalDPx7pVpYQ', + y : '5cAlBmnzzuwRNuFtLhyFNdy9v1rVEqEgrFEiiwKMx5I', + kid : 'jGYs9XgQMDH_PCDFWocTN0F06mTUOA1J1McVvluq4lM', + alg : 'ES256K', + }, + }, + ], + authentication: [ + 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#0', + 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#sig', + ], + assertionMethod: [ + 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#0', + 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#sig', + ], + capabilityDelegation: [ + 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#0', + ], + capabilityInvocation: [ + 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#0', + ], + keyAgreement: [ + 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#enc', + ], + service: [ + { + id : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#dwn', + type : 'DecentralizedWebNode', + serviceEndpoint : 'https://example.com/dwn', + enc : '#enc', + sig : '#sig', + }, + ], + }, + didMetadata: { + published: false, + } + }); + + for (const record of dnsPacket.answers ?? []) { + if (record.name.startsWith('_s')) { + expect(record.data).to.include('id=dwn'); + expect(record.data).to.include('t=DecentralizedWebNode'); + expect(record.data).to.include('se=https://example.com/dwn'); + expect(record.data).to.include('enc=#enc'); + expect(record.data).to.include('sig=#sig'); + } + } + }); + + it('handles custom array for services', async () => { + const dnsPacket = await DidDhtDocument.toDnsPacket({ + didDocument: { + id : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery', + verificationMethod : [ + { + id : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#0', + type : 'JsonWebKey', + controller : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery', + publicKeyJwk : { + crv : 'Ed25519', + kty : 'OKP', + x : '2zHGF5m_DhcPbBZB6ooIxIOR-Vw-yJVYSPo2NgCMkgg', + kid : 'KDT9PKj4_z7gPk2s279Y-OGlMtt_L93oJzIaiVrrySU', + alg : 'EdDSA', + }, + }, + { + id : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#sig', + type : 'JsonWebKey', + controller : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery', + publicKeyJwk : { + crv : 'Ed25519', + kty : 'OKP', + x : 'FrrBhqvAWxE4lstj-IWgN8_5-O4L1KuZjdNjn5bX_dw', + kid : 'dRnxo2XQ7QT1is5WmpEefwEz3z4_4JdpGea6KWUn3ww', + alg : 'EdDSA', + }, + }, + { + id : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#enc', + type : 'JsonWebKey', + controller : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery', + publicKeyJwk : { + kty : 'EC', + crv : 'secp256k1', + x : 'e1_pCWZwI9cxdrotVKIT8t75itk22XkpalDPx7pVpYQ', + y : '5cAlBmnzzuwRNuFtLhyFNdy9v1rVEqEgrFEiiwKMx5I', + kid : 'jGYs9XgQMDH_PCDFWocTN0F06mTUOA1J1McVvluq4lM', + alg : 'ES256K', + }, + }, + ], + authentication: [ + 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#0', + 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#sig', + ], + assertionMethod: [ + 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#0', + 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#sig', + ], + capabilityDelegation: [ + 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#0', + ], + capabilityInvocation: [ + 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#0', + ], + keyAgreement: [ + 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#enc', + ], + service: [ + { + id : 'did:dht:5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery#dwn', + type : 'DecentralizedWebNode', + serviceEndpoint : 'https://example.com/dwn', + enc : '#enc', + sig : ['#sig', '#0'], + }, + ], + }, + didMetadata: { + published: false, + } + }); + + for (const record of dnsPacket.answers ?? []) { + if (record.name.startsWith('_s')) { + expect(record.data).to.include('id=dwn'); + expect(record.data).to.include('t=DecentralizedWebNode'); + expect(record.data).to.include('se=https://example.com/dwn'); + expect(record.data).to.include('enc=#enc'); + expect(record.data).to.include('sig=#sig,#0'); + } } }); });