diff --git a/packages/dids/src/did-error.ts b/packages/dids/src/did-error.ts index efc0b93d5..9c8b672b8 100644 --- a/packages/dids/src/did-error.ts +++ b/packages/dids/src/did-error.ts @@ -61,6 +61,9 @@ export enum DidErrorCode { /** The DID resolver was unable to find the DID document resulting from the resolution request. */ NotFound = 'notFound', + /** The DID resolver was unable to find the DID document in any authoritative gateway. */ + NotFoundInAuthoritativeGateWay = 'notFoundInAuthoritativeGateWay', + /** * The representation requested via the `accept` input metadata property is not supported by the * DID method and/or DID resolver implementation. diff --git a/packages/dids/src/methods/did-dht.ts b/packages/dids/src/methods/did-dht.ts index 53fc09623..d14761554 100644 --- a/packages/dids/src/methods/did-dht.ts +++ b/packages/dids/src/methods/did-dht.ts @@ -805,10 +805,40 @@ export class DidDhtDocument { const publicKeyBytes = DidDhtUtils.identifierToIdentityKeyBytes({ didUri }); // Retrieve the signed BEP44 message from a DID DHT Gateway or Pkarr relay. - const bep44Message = await DidDhtDocument.pkarrGet({ gatewayUri, publicKeyBytes }); + let bep44Message = await DidDhtDocument.pkarrGet({ gatewayUri, publicKeyBytes }); // Verify the signature of the BEP44 message and parse the value to a DNS packet. - const dnsPacket = await DidDhtUtils.parseBep44GetMessage({ bep44Message }); + let dnsPacket: Packet | undefined = await DidDhtUtils.parseBep44GetMessage({ bep44Message }); + + // Look at the NS records in the DNS packet to find the resolution gateway URIs. + const resolutionGatewayUris = await DidDhtDocument.getAuthoritativeGatewayUris({ didUri, dnsPacket }); + + // Only do a second retrieval if authoritative resolution gateway URIs are specified and are different from the given gateway URI. + if(resolutionGatewayUris.length > 0 && !resolutionGatewayUris.includes(gatewayUri)) { + dnsPacket = undefined; // reset to `undefined` to use as a condition check for throwing an error + const accumulatedErrors = []; + for(const nsRecordGatewayUri of resolutionGatewayUris) { + try { + bep44Message = await DidDhtDocument.pkarrGet({ gatewayUri: nsRecordGatewayUri, publicKeyBytes }); + dnsPacket = await DidDhtUtils.parseBep44GetMessage({ bep44Message }); + } catch (error: any) { + accumulatedErrors.push(`Failed retrieval from ${nsRecordGatewayUri}: ${error}`); + + // If the retrieval failed, try the next resolution gateway. + continue; + } + + // If the retrieval was successful, break the loop. + break; + } + + if(dnsPacket === undefined) { + throw new DidError( + DidErrorCode.NotFoundInAuthoritativeGateWay, + `DID document not found for: ${didUri} after looping through all authoritative gateways. Errors: ${accumulatedErrors.join('; ')}` + ); + } + } // Convert the DNS packet to a DID document and metadata. const resolutionResult = await DidDhtDocument.fromDnsPacket({ didUri, dnsPacket }); @@ -965,6 +995,37 @@ export class DidDhtDocument { return response.ok; } + + /** + * Extracts authoritative gateway URIs from a DNS packet based on the DID DHT specifications. + * This method filters NS records related to the provided DID URI and extracts gateway URIs + * that are used to resolve the complete DID document. + * + * @see {@link https://did-dht.com/#designating-authoritative-gateways | DID DHT Specification, ยง Authoritative Gateways} + * + * @param {object} params - The parameters to use when extracting gateway URIs from the DNS packet. + * @param {string} params.didUri - The DID URI corresponding to the DNS packet. + * @param {Packet} params.dnsPacket - The DNS packet containing potential NS records for resolution gateways. + * @returns {Promise} Resolves to an array of gateway URIs without trailing '.' if found, otherwise an empty array. + */ + public static async getAuthoritativeGatewayUris({ didUri, dnsPacket }: { + didUri: string; + dnsPacket: Packet; + }): Promise { + const authoritativeGatewayUris: string[] = []; + + for (const answer of dnsPacket?.answers ?? []) { + if (answer.type !== 'NS') continue; + + if (answer.name.endsWith(`.${DidDhtDocument.getUniqueDidSuffix(didUri)}.`)) { + const gatewayUri = answer.data.slice(0, -1); // Remove trailing dot + authoritativeGatewayUris.push(gatewayUri); + } + } + + return authoritativeGatewayUris; + } + /** * Converts a DNS packet to a DID document according to the DID DHT specification. * diff --git a/packages/dids/tests/methods/did-dht.spec.ts b/packages/dids/tests/methods/did-dht.spec.ts index 5bc39dcd5..a03ff684b 100644 --- a/packages/dids/tests/methods/did-dht.spec.ts +++ b/packages/dids/tests/methods/did-dht.spec.ts @@ -1157,6 +1157,92 @@ describe('DidDhtDocument', () => { } } }); + + it('handles custom authoritative gateways', async () => { + + const didDocument: 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', + ], + }; + + const authoritativeGatewayUris = ['gateway1.example-did-dht-gateway.com', 'gateway2.example-did-dht-gateway.com']; + + const dnsPacket = await DidDhtDocument.toDnsPacket({ + didDocument, + didMetadata: { + published: true, + }, + authoritativeGatewayUris + }); + + for (const record of dnsPacket.answers ?? []) { + if (record.type !== 'NS') + continue; + + expect(record.name).to.equal('_did.5cahcfh3zh8bqd5cn3y6inoea1b3d6kh85rjksne9e5dcyrc1ery.'); + expect(record.data).to.match(/(gateway1.example-did-dht-gateway.com.|gateway2.example-did-dht-gateway.com.)/); + } + + const resolutionGatewayUris = await DidDhtDocument.getAuthoritativeGatewayUris({ didUri: didDocument.id, dnsPacket }); + + expect(resolutionGatewayUris).to.have.length(2); + expect(resolutionGatewayUris).to.have.members(authoritativeGatewayUris); + }); }); describe('Web5TestVectorsDidDht', () => {