From e7fabdd66b117fc7f2b643ecb0376685f02a8245 Mon Sep 17 00:00:00 2001 From: Henry Tsai Date: Tue, 30 Apr 2024 18:50:25 -0700 Subject: [PATCH 1/7] Updated DID:DHT implementation to be compliant with vector 1 --- packages/dids/.vscode/launch.json | 2 +- packages/dids/src/methods/did-dht.ts | 10 ++-- packages/dids/src/types/did-core.ts | 16 +++---- .../did-dht/vector-1-did-document.json | 29 +++++++++++ .../did-dht/vector-1-dns-records.json | 14 ++++++ packages/dids/tests/methods/did-dht.spec.ts | 48 ++++++++++++++++--- web5-js.code-workspace | 6 ++- 7 files changed, 105 insertions(+), 20 deletions(-) create mode 100644 packages/dids/tests/fixtures/test-vectors/did-dht/vector-1-did-document.json create mode 100644 packages/dids/tests/fixtures/test-vectors/did-dht/vector-1-dns-records.json diff --git a/packages/dids/.vscode/launch.json b/packages/dids/.vscode/launch.json index 05c7ab34d..e74b7449e 100644 --- a/packages/dids/.vscode/launch.json +++ b/packages/dids/.vscode/launch.json @@ -8,7 +8,7 @@ "type": "node", "request": "launch", "name": "test:node", - "runtimeExecutable": "${workspaceFolder:dids}/node_modules/.bin/mocha", + "runtimeExecutable": "${workspaceFolder:dids}/../../node_modules/.bin/mocha", "console": "internalConsole", "preLaunchTask": "build tests", "skipFiles": [ diff --git a/packages/dids/src/methods/did-dht.ts b/packages/dids/src/methods/did-dht.ts index ce2bd1468..508dcf3de 100644 --- a/packages/dids/src/methods/did-dht.ts +++ b/packages/dids/src/methods/did-dht.ts @@ -1260,10 +1260,14 @@ export class DidDhtDocument { data : rootRecord.join(PROPERTY_SEPARATOR) }); - // Per the DID DHT specification, the method-specific identifier must be appended as the - // Origin of all records. + // Per the DID DHT specification (https://did-dht.com/#dids-as-dns-records): + // Record name of a Root Record or a Authoritative Gateway Records MUST end in `.` const [, , identifier] = didDocument.id.split(':'); - dnsAnswerRecords.forEach(record => record.name += identifier); + dnsAnswerRecords.forEach(record => { + if (record.name.startsWith('_did.')) { + record.name += identifier + '.'; + } + }); // Create a DNS response packet with the authoritative answer flag set. const dnsPacket: Packet = { diff --git a/packages/dids/src/types/did-core.ts b/packages/dids/src/types/did-core.ts index 1e82db6c7..fd4c501a1 100644 --- a/packages/dids/src/types/did-core.ts +++ b/packages/dids/src/types/did-core.ts @@ -563,18 +563,18 @@ export enum DidVerificationRelationship { keyAgreement = 'keyAgreement', /** - * Specifies a mechanism used by the DID subject to delegate a cryptographic capability to another - * party. This can include delegating access to a specific resource or API. + * Specifies a verification method used by the DID subject to invoke a cryptographic capability. + * This is frequently associated with authorization actions, like updating the DID Document. * - * @see {@link https://www.w3.org/TR/did-core/#capability-delegation | DID Core Specification, § Capability Delegation} + * @see {@link https://www.w3.org/TR/did-core/#capability-invocation | DID Core Specification, § Capability Invocation} */ - capabilityDelegation = 'capabilityDelegation', + capabilityInvocation = 'capabilityInvocation', /** - * Specifies a verification method used by the DID subject to invoke a cryptographic capability. - * This is frequently associated with authorization actions, like updating the DID Document. + * Specifies a mechanism used by the DID subject to delegate a cryptographic capability to another + * party. This can include delegating access to a specific resource or API. * - * @see {@link https://www.w3.org/TR/did-core/#capability-invocation | DID Core Specification, § Capability Invocation} + * @see {@link https://www.w3.org/TR/did-core/#capability-delegation | DID Core Specification, § Capability Delegation} */ - capabilityInvocation = 'capabilityInvocation' + capabilityDelegation = 'capabilityDelegation', } \ No newline at end of file diff --git a/packages/dids/tests/fixtures/test-vectors/did-dht/vector-1-did-document.json b/packages/dids/tests/fixtures/test-vectors/did-dht/vector-1-did-document.json new file mode 100644 index 000000000..3b20263c6 --- /dev/null +++ b/packages/dids/tests/fixtures/test-vectors/did-dht/vector-1-did-document.json @@ -0,0 +1,29 @@ +{ + "id": "did:dht:cyuoqaf7itop8ohww4yn5ojg13qaq83r9zihgqntc5i9zwrfdfoo", + "verificationMethod": [ + { + "id": "did:dht:cyuoqaf7itop8ohww4yn5ojg13qaq83r9zihgqntc5i9zwrfdfoo#0", + "type": "JsonWebKey", + "controller": "did:dht:cyuoqaf7itop8ohww4yn5ojg13qaq83r9zihgqntc5i9zwrfdfoo", + "publicKeyJwk": { + "kid": "0", + "alg": "Ed25519", + "crv": "Ed25519", + "kty": "OKP", + "x": "YCcHYL2sYNPDlKaALcEmll2HHyT968M4UWbr-9CFGWE" + } + } + ], + "authentication": [ + "did:dht:cyuoqaf7itop8ohww4yn5ojg13qaq83r9zihgqntc5i9zwrfdfoo#0" + ], + "assertionMethod": [ + "did:dht:cyuoqaf7itop8ohww4yn5ojg13qaq83r9zihgqntc5i9zwrfdfoo#0" + ], + "capabilityInvocation": [ + "did:dht:cyuoqaf7itop8ohww4yn5ojg13qaq83r9zihgqntc5i9zwrfdfoo#0" + ], + "capabilityDelegation": [ + "did:dht:cyuoqaf7itop8ohww4yn5ojg13qaq83r9zihgqntc5i9zwrfdfoo#0" + ] +} \ No newline at end of file diff --git a/packages/dids/tests/fixtures/test-vectors/did-dht/vector-1-dns-records.json b/packages/dids/tests/fixtures/test-vectors/did-dht/vector-1-dns-records.json new file mode 100644 index 000000000..a749bfd0d --- /dev/null +++ b/packages/dids/tests/fixtures/test-vectors/did-dht/vector-1-dns-records.json @@ -0,0 +1,14 @@ +[ + { + "name": "_did.cyuoqaf7itop8ohww4yn5ojg13qaq83r9zihgqntc5i9zwrfdfoo.", + "type": "TXT", + "ttl": 7200, + "rdata": "v=0;vm=k0;auth=k0;asm=k0;inv=k0;del=k0" + }, + { + "name": "_k0._did.", + "type": "TXT", + "ttl": 7200, + "rdata": "id=0;t=0;k=YCcHYL2sYNPDlKaALcEmll2HHyT968M4UWbr-9CFGWE" + } +] \ No newline at end of file diff --git a/packages/dids/tests/methods/did-dht.spec.ts b/packages/dids/tests/methods/did-dht.spec.ts index 61f5db026..28eed022d 100644 --- a/packages/dids/tests/methods/did-dht.spec.ts +++ b/packages/dids/tests/methods/did-dht.spec.ts @@ -1,14 +1,16 @@ +import type { PortableDid } from '../../src/types/portable-did.js'; + import sinon from 'sinon'; +import resolveTestVectors from '../../../../web5-spec/test-vectors/did_dht/resolve.json' assert { type: 'json' }; +import officialTestVector1DidDocument from '../fixtures/test-vectors/did-dht/vector-1-did-document.json' assert { type: 'json' }; +import officialTestVector1DnsRecords from '../fixtures/test-vectors/did-dht/vector-1-dns-records.json' assert { type: 'json' }; + import { expect } from 'chai'; import { Convert } from '@web5/common'; - -import type { PortableDid } from '../../src/types/portable-did.js'; - +import { DidDocument } from '../../src/index.js'; import { DidErrorCode } from '../../src/did-error.js'; import { DidDht, DidDhtDocument, DidDhtRegisteredDidType } from '../../src/methods/did-dht.js'; -import DidDhtResolveTestVector from '../../../../web5-spec/test-vectors/did_dht/resolve.json' assert { type: 'json' }; - // Helper function to create a mocked fetch response that fails and returns a 404 Not Found. const fetchNotFoundResponse = () => ({ status : 404, @@ -431,10 +433,15 @@ describe('DidDht', () => { it('throws an error if the resulting DID document would exceed the 1000 byte maximum', async () => { try { - // Attempt to create a DID with 6 verification methods (Identity Key plus 5 additional). + // Attempt to create a DID with DID Document that exceeds the maximum size. await DidDht.create({ options: { verificationMethods: [ + { algorithm: 'Ed25519' }, + { algorithm: 'Ed25519' }, + { algorithm: 'Ed25519' }, + { algorithm: 'Ed25519' }, + { algorithm: 'Ed25519' }, { algorithm: 'Ed25519' }, { algorithm: 'Ed25519' }, { algorithm: 'Ed25519' }, @@ -1153,10 +1160,37 @@ describe('DidDhtDocument', () => { describe('Web5TestVectorsDidDht', () => { it('resolve', async () => { - for (const vector of DidDhtResolveTestVector.vectors as any[]) { + for (const vector of resolveTestVectors.vectors as any[]) { const didResolutionResult = await DidDht.resolve(vector.input.didUri); expect(didResolutionResult.didResolutionMetadata.error).to.equal(vector.output.didResolutionMetadata.error); } }).timeout(30000); // Set timeout to 30 seconds for this test for did:dht resolution timeout test }); }); + +// vectors come from https://did-dht.com/#test-vectors +describe('Official DID:DHT Vector tests', () => { + it('vector 1', async () => { + const dnsPacket = await DidDhtDocument.toDnsPacket({ + didDocument : officialTestVector1DidDocument as DidDocument, + didMetadata : { published: false } + }); + + expect(dnsPacket.answers).to.have.length(officialTestVector1DnsRecords.length); + + // NOTE: the DNS library we use uses name `data` instead of `rdata` seen in the test vectors hence the additional normalization step + const normalizedConstructedRecords = []; + for (const record of dnsPacket.answers!) { + const { data: rdata, ...otherProperties } = record; + + const normalizedRecord = { + ... otherProperties, + rdata + }; + + normalizedConstructedRecords.push(normalizedRecord); + } + + expect(normalizedConstructedRecords).to.deep.include.members(officialTestVector1DnsRecords); + }); +}); diff --git a/web5-js.code-workspace b/web5-js.code-workspace index 8fa18afd1..76c1fa989 100644 --- a/web5-js.code-workspace +++ b/web5-js.code-workspace @@ -66,7 +66,11 @@ "editor.codeActionsOnSave": { "source.fixAll": "always" }, - "typescript.tsdk": "root/node_modules/typescript/lib" + "typescript.tsdk": "root/node_modules/typescript/lib", + "search.exclude": { + "**/dist/**": true, + "**/coverage/**": true + } }, "launch": { "version": "0.2.0", From 39706301acf925630d88daf635905c196a937693 Mon Sep 17 00:00:00 2001 From: Henry Tsai Date: Tue, 30 Apr 2024 18:59:24 -0700 Subject: [PATCH 2/7] minor refactoring --- packages/dids/tests/methods/did-dht.spec.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/dids/tests/methods/did-dht.spec.ts b/packages/dids/tests/methods/did-dht.spec.ts index 28eed022d..dca2cde3f 100644 --- a/packages/dids/tests/methods/did-dht.spec.ts +++ b/packages/dids/tests/methods/did-dht.spec.ts @@ -1178,18 +1178,16 @@ describe('Official DID:DHT Vector tests', () => { expect(dnsPacket.answers).to.have.length(officialTestVector1DnsRecords.length); - // NOTE: the DNS library we use uses name `data` instead of `rdata` seen in the test vectors hence the additional normalization step - const normalizedConstructedRecords = []; - for (const record of dnsPacket.answers!) { + // NOTE: the DNS library we use uses name `data` instead of `rdata` used in DID:DHT spec, + // but prefer to keep the naming in test vector files identical that of the DID:DHT spec, + // hence this additional normalization step + const normalizedConstructedRecords = dnsPacket.answers!.map(record => { const { data: rdata, ...otherProperties } = record; - - const normalizedRecord = { - ... otherProperties, + return { + ...otherProperties, rdata }; - - normalizedConstructedRecords.push(normalizedRecord); - } + }); expect(normalizedConstructedRecords).to.deep.include.members(officialTestVector1DnsRecords); }); From 079e1325c5143711a592f56ac308967eb3767ae3 Mon Sep 17 00:00:00 2001 From: Henry Tsai Date: Tue, 30 Apr 2024 19:06:25 -0700 Subject: [PATCH 3/7] changeset --- .changeset/two-moons-agree.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/two-moons-agree.md diff --git a/.changeset/two-moons-agree.md b/.changeset/two-moons-agree.md new file mode 100644 index 000000000..7c07d8d04 --- /dev/null +++ b/.changeset/two-moons-agree.md @@ -0,0 +1,5 @@ +--- +"@web5/dids": patch +--- + +DID:DHT - Only have . suffix for Root and Gateway Record names From 9774d64ee592cdd79f0e985c2aaf24abe4af8a2a Mon Sep 17 00:00:00 2001 From: Henry Tsai Date: Wed, 1 May 2024 09:32:40 -0700 Subject: [PATCH 4/7] typo --- packages/dids/tests/methods/did-dht.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dids/tests/methods/did-dht.spec.ts b/packages/dids/tests/methods/did-dht.spec.ts index dca2cde3f..929362888 100644 --- a/packages/dids/tests/methods/did-dht.spec.ts +++ b/packages/dids/tests/methods/did-dht.spec.ts @@ -1179,7 +1179,7 @@ describe('Official DID:DHT Vector tests', () => { expect(dnsPacket.answers).to.have.length(officialTestVector1DnsRecords.length); // NOTE: the DNS library we use uses name `data` instead of `rdata` used in DID:DHT spec, - // but prefer to keep the naming in test vector files identical that of the DID:DHT spec, + // but prefer to keep the naming in test vector files identical to that of the DID:DHT spec, // hence this additional normalization step const normalizedConstructedRecords = dnsPacket.answers!.map(record => { const { data: rdata, ...otherProperties } = record; From 71144bf25fe4ddfa774aabcb57a78f595ab6e0b3 Mon Sep 17 00:00:00 2001 From: Henry Tsai Date: Wed, 1 May 2024 09:38:51 -0700 Subject: [PATCH 5/7] search exclude one more directory --- web5-js.code-workspace | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/web5-js.code-workspace b/web5-js.code-workspace index 76c1fa989..ef7448c45 100644 --- a/web5-js.code-workspace +++ b/web5-js.code-workspace @@ -69,7 +69,9 @@ "typescript.tsdk": "root/node_modules/typescript/lib", "search.exclude": { "**/dist/**": true, - "**/coverage/**": true + "**/coverage/**": true, + "**/compiled/**": true + } }, "launch": { From 34bb78db310f729dd05c96b80a29fdcd0fe3810b Mon Sep 17 00:00:00 2001 From: Henry Tsai Date: Wed, 1 May 2024 09:54:39 -0700 Subject: [PATCH 6/7] Added self to CODEOWNERS default owners --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 5e71ff8b1..e97b72699 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -6,7 +6,7 @@ # The format is described: https://github.blog/2017-07-06-introducing-code-owners/ # These owners will be the default owners for everything in the repo. -* @frankhinek @csuwildcat @mistermoe +* @frankhinek @csuwildcat @mistermoe @thehenrytsai # These are owners of any file in the `common`, `crypto`, `crypto-aws-kms`, `dids`, and # `credentials` packages and their sub-directories. From 727295556127a2d030668b078fca52650ac7fb81 Mon Sep 17 00:00:00 2001 From: Henry Tsai Date: Wed, 1 May 2024 13:31:13 -0700 Subject: [PATCH 7/7] Address review comments --- packages/dids/src/methods/did-dht.ts | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/dids/src/methods/did-dht.ts b/packages/dids/src/methods/did-dht.ts index 508dcf3de..00dbfafad 100644 --- a/packages/dids/src/methods/did-dht.ts +++ b/packages/dids/src/methods/did-dht.ts @@ -1255,20 +1255,11 @@ export class DidDhtDocument { // Add a DNS TXT record for the root record. dnsAnswerRecords.push({ type : 'TXT', - name : '_did.', + name : '_did.' + DidDhtDocument.getUniqueDidSuffix(didDocument.id) + '.', // name of a Root Record MUST end in `.` ttl : DNS_RECORD_TTL, data : rootRecord.join(PROPERTY_SEPARATOR) }); - // Per the DID DHT specification (https://did-dht.com/#dids-as-dns-records): - // Record name of a Root Record or a Authoritative Gateway Records MUST end in `.` - const [, , identifier] = didDocument.id.split(':'); - dnsAnswerRecords.forEach(record => { - if (record.name.startsWith('_did.')) { - record.name += identifier + '.'; - } - }); - // Create a DNS response packet with the authoritative answer flag set. const dnsPacket: Packet = { id : 0, @@ -1279,6 +1270,16 @@ export class DidDhtDocument { return dnsPacket; } + + /** + * Gets the unique portion of the DID identifier after the last `:` character. + * e.g. `did:dht:example` -> `example` + * + * @param did - The DID to extract the unique suffix from. + */ + private static getUniqueDidSuffix(did: string ): string { + return did.split(':')[2]; + } } /**