From 304c1ccf5d319934aab937dd73eb2ef57d6623ec Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Tue, 5 Dec 2023 14:23:50 -0500 Subject: [PATCH 1/3] Improve error handling, align key set with other methods, and increase test coverage Signed-off-by: Frank Hinek --- .../tests/verifiable-credential.spec.ts | 6 +- packages/dids/src/did-dht.ts | 93 +++-- packages/dids/tests/dht.spec.ts | 19 +- packages/dids/tests/did-dht.spec.ts | 348 +++++++++++++++--- 4 files changed, 356 insertions(+), 110 deletions(-) diff --git a/packages/credentials/tests/verifiable-credential.spec.ts b/packages/credentials/tests/verifiable-credential.spec.ts index 6cc2fe02a..50d28a14a 100644 --- a/packages/credentials/tests/verifiable-credential.spec.ts +++ b/packages/credentials/tests/verifiable-credential.spec.ts @@ -276,7 +276,7 @@ describe('Verifiable Credential Tests', () => { ] } }; - const didDhtCreateSpy = sinon.stub(DidDhtMethod, 'create').resolves(mockDocument); + const didDhtCreateStub = sinon.stub(DidDhtMethod, 'create').resolves(mockDocument); const alice = await DidDhtMethod.create({ publish: true }); @@ -343,8 +343,8 @@ describe('Verifiable Credential Tests', () => { await VerifiableCredential.verify(vcJwt); - sinon.assert.calledOnce(didDhtCreateSpy); - sinon.assert.calledOnce(dhtDidResolutionSpy); + expect(didDhtCreateStub.calledOnce).to.be.true; + expect(dhtDidResolutionSpy.calledOnce).to.be.true; sinon.restore(); }); }); diff --git a/packages/dids/src/did-dht.ts b/packages/dids/src/did-dht.ts index 2e19e860d..3d6ad7aef 100644 --- a/packages/dids/src/did-dht.ts +++ b/packages/dids/src/did-dht.ts @@ -29,7 +29,6 @@ export type DidDhtCreateOptions = { } export type DidDhtKeySet = { - identityKey?: JwkKeyPair; verificationMethodKeys?: DidKeySetVerificationMethodKey[]; } @@ -43,13 +42,14 @@ export class DidDhtMethod implements DidMethod { * @returns A promise that resolves to a PortableDid object. */ public static async create(options?: DidDhtCreateOptions): Promise { - const { publish, keySet: initialKeySet, services } = options ?? {}; + const { publish = false, keySet: initialKeySet, services } = options ?? {}; // 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 }); + const identityKey = keySet.verificationMethodKeys.find(key => key.publicKeyJwk.kid === '0'); + const id = await this.getDidIdentifier({ key: identityKey.publicKeyJwk }); // Add all other keys to the verificationMethod and relationship arrays. const relationshipsMap: Partial> = {}; @@ -74,16 +74,20 @@ export class DidDhtMethod implements DidMethod { services?.map(service => { service.id = `${id}#${service.id}`; }); + + // Assemble the DID Document. const document: DidDocument = { id, verificationMethod: [...verificationMethods], ...relationshipsMap, - ...services && {service: services} + ...services && { service: services } }; + // If the publish flag is set, publish the DID Document to the DHT. if (publish) { - await this.publish({ keySet, didDocument: document }); + await this.publish({ identityKey, didDocument: document }); } + return { did : document.id, document : document, @@ -156,35 +160,25 @@ export class DidDhtMethod implements DidMethod { }): Promise { let { keySet = {} } = options ?? {}; - if (!keySet.identityKey) { - keySet.identityKey = await this.generateJwkKeyPair({ + // If the key set is missing a `verificationMethodKeys` array, create one. + if (!keySet.verificationMethodKeys) keySet.verificationMethodKeys = []; + + // If the key set lacks an identity key (`kid: 0`), generate one. + if (!keySet.verificationMethodKeys.some(key => key.publicKeyJwk.kid === '0')) { + const identityKey = await this.generateJwkKeyPair({ keyAlgorithm : 'Ed25519', keyId : '0' }); - - - } else if (keySet.identityKey.publicKeyJwk.kid !== '0') { - throw new Error('The identity key must have a kid of 0'); - } - - // add verificationMethodKeys for the identity key - const identityKeySetVerificationMethod: DidKeySetVerificationMethodKey = { - ...keySet.identityKey, - relationships: ['authentication', 'assertionMethod', 'capabilityInvocation', 'capabilityDelegation'] - }; - - if (!keySet.verificationMethodKeys) { - keySet.verificationMethodKeys = [identityKeySetVerificationMethod]; - } else if (keySet.verificationMethodKeys.filter(key => key.publicKeyJwk.kid === '0').length === 0) { - keySet.verificationMethodKeys.push(identityKeySetVerificationMethod); + keySet.verificationMethodKeys.push({ + ...identityKey, + relationships: ['authentication', 'assertionMethod', 'capabilityInvocation', 'capabilityDelegation'] + }); } // Generate RFC 7638 JWK thumbprints if `kid` is missing from any key. - if (keySet.verificationMethodKeys) { - for (const key of keySet.verificationMethodKeys) { - if (key.publicKeyJwk) key.publicKeyJwk.kid ??= await Jose.jwkThumbprint({key: key.publicKeyJwk}); - if (key.privateKeyJwk) key.privateKeyJwk.kid ??= await Jose.jwkThumbprint({key: key.privateKeyJwk}); - } + for (const key of keySet.verificationMethodKeys) { + if (key.publicKeyJwk) key.publicKeyJwk.kid ??= await Jose.jwkThumbprint({key: key.publicKeyJwk}); + if (key.privateKeyJwk) key.privateKeyJwk.kid ??= await Jose.jwkThumbprint({key: key.privateKeyJwk}); } return keySet; @@ -198,9 +192,9 @@ export class DidDhtMethod implements DidMethod { public static async getDidIdentifier(options: { key: PublicKeyJwk }): Promise { - const {key} = options; + const { key } = options; - const cryptoKey = await Jose.jwkToCryptoKey({key}); + const cryptoKey = await Jose.jwkToCryptoKey({ key }); const identifier = z32.encode(cryptoKey.material); return 'did:dht:' + identifier; } @@ -213,8 +207,8 @@ export class DidDhtMethod implements DidMethod { public static async getDidIdentifierFragment(options: { key: PublicKeyJwk }): Promise { - const {key} = options; - const cryptoKey = await Jose.jwkToCryptoKey({key}); + const { key } = options; + const cryptoKey = await Jose.jwkToCryptoKey({ key }); return z32.encode(cryptoKey.material); } @@ -224,12 +218,12 @@ export class DidDhtMethod implements DidMethod { * @param didDocument The DID Document to publish. * @returns A boolean indicating the success of the publishing operation. */ - public static async publish({ didDocument, keySet }: { + public static async publish({ didDocument, identityKey }: { didDocument: DidDocument, - keySet: DidDhtKeySet + identityKey: DidKeySetVerificationMethodKey }): Promise { - const publicCryptoKey = await Jose.jwkToCryptoKey({key: keySet.identityKey.publicKeyJwk}); - const privateCryptoKey = await Jose.jwkToCryptoKey({key: keySet.identityKey.privateKeyJwk}); + const publicCryptoKey = await Jose.jwkToCryptoKey({key: identityKey.publicKeyJwk}); + const privateCryptoKey = await Jose.jwkToCryptoKey({key: identityKey.privateKeyJwk}); const isPublished = await DidDht.publishDidDocument({ keyPair: { @@ -261,7 +255,7 @@ export class DidDhtMethod implements DidMethod { if (!parsedDid) { return { '@context' : 'https://w3id.org/did-resolution/v1', - didDocument : undefined, + didDocument : null, didDocumentMetadata : {}, didResolutionMetadata : { contentType : 'application/did+ld+json', @@ -274,7 +268,7 @@ export class DidDhtMethod implements DidMethod { if (parsedDid.method !== 'dht') { return { '@context' : 'https://w3id.org/did-resolution/v1', - didDocument : undefined, + didDocument : null, didDocumentMetadata : {}, didResolutionMetadata : { contentType : 'application/did+ld+json', @@ -284,7 +278,28 @@ export class DidDhtMethod implements DidMethod { }; } - const didDocument = await DidDht.getDidDocument({ did: parsedDid.did }); + let didDocument: DidDocument; + + /** + * TODO: This is a temporary workaround for the following issue: https://github.com/TBD54566975/web5-js/issues/331 + * As of 5 Dec 2023, the `pkarr` library throws an error if the DID is not found. Until a + * better solution is found, catch the error and return a DID Resolution Result with an + * error message. + */ + try { + didDocument = await DidDht.getDidDocument({ did: parsedDid.did }); + } catch (error: any) { + return { + '@context' : 'https://w3id.org/did-resolution/v1', + didDocument : null, + didDocumentMetadata : {}, + didResolutionMetadata : { + contentType : 'application/did+ld+json', + error : 'internalError', + errorMessage : `An unexpected error occurred while resolving DID: ${parsedDid.did}` + } + }; + } return { '@context' : 'https://w3id.org/did-resolution/v1', diff --git a/packages/dids/tests/dht.spec.ts b/packages/dids/tests/dht.spec.ts index 157a7569f..ea2f5cd7e 100644 --- a/packages/dids/tests/dht.spec.ts +++ b/packages/dids/tests/dht.spec.ts @@ -1,8 +1,7 @@ +import sinon from 'sinon'; import { expect } from 'chai'; import { Jose } from '@web5/crypto'; -import sinon from 'sinon'; -import type { DidDhtKeySet } from '../src/did-dht.js'; import type { DidKeySetVerificationMethodKey, DidService } from '../src/types.js'; import { DidDht } from '../src/dht.js'; @@ -12,12 +11,12 @@ describe('DidDht', () => { it('should create a put and parse a get request', async () => { 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 identityKey = keySet.verificationMethodKeys.find(key => key.publicKeyJwk.kid === '0'); + const publicCryptoKey = await Jose.jwkToCryptoKey({ key: identityKey.publicKeyJwk }); + const privateCryptoKey = await Jose.jwkToCryptoKey({ key: identityKey.privateKeyJwk }); - const dhtPublishSpy = sinon.stub(DidDht, 'publishDidDocument').resolves(true); - const dhtGetSpy = sinon.stub(DidDht, 'getDidDocument').resolves(document); + const dhtPublishStub = sinon.stub(DidDht, 'publishDidDocument').resolves(true); + const dhtGetStub = sinon.stub(DidDht, 'getDidDocument').resolves(document); const published = await DidDht.publishDidDocument({ keyPair: { @@ -42,8 +41,8 @@ describe('DidDht', () => { 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); - sinon.assert.calledOnce(dhtPublishSpy); - sinon.assert.calledOnce(dhtGetSpy); + expect(dhtPublishStub.calledOnce).to.be.true; + expect(dhtGetStub.calledOnce).to.be.true; sinon.restore(); }); @@ -85,4 +84,4 @@ describe('DidDht', () => { 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 5ce2b300a..507ae4e1f 100644 --- a/packages/dids/tests/did-dht.spec.ts +++ b/packages/dids/tests/did-dht.spec.ts @@ -1,17 +1,22 @@ +import type { PublicKeyJwk } from '@web5/crypto'; + +import sinon from 'sinon'; import chai, { expect } from 'chai'; import chaiAsPromised from 'chai-as-promised'; -import sinon from 'sinon'; import type { DidDhtKeySet } from '../src/did-dht.js'; -import type {DidKeySetVerificationMethodKey, DidService, PortableDid} from '../src/types.js'; +import type { DidDocument, DidKeySetVerificationMethodKey, DidService, PortableDid } from '../src/types.js'; +import { DidDht } from '../src/dht.js'; +import { parseDid } from '../src/utils.js'; import { DidDhtMethod } from '../src/did-dht.js'; +import { DidResolver } from '../src/did-resolver.js'; chai.use(chaiAsPromised); describe('DidDhtMethod', () => { - describe('keypairs', () => { - it('should generate a key pair', async () => { + describe('generateJwkKeyPair()', () => { + it('generates Ed25519 JWK key pairs', async () => { const ed25519KeyPair = await DidDhtMethod.generateJwkKeyPair({ keyAlgorithm: 'Ed25519' }); expect(ed25519KeyPair).to.exist; @@ -20,7 +25,9 @@ describe('DidDhtMethod', () => { expect(ed25519KeyPair.publicKeyJwk.kid).to.exist; expect(ed25519KeyPair.publicKeyJwk.alg).to.equal('EdDSA'); expect(ed25519KeyPair.publicKeyJwk.kty).to.equal('OKP'); + }); + it('generates secp256k1 JWK key pairs', async () => { const secp256k1KeyPair = await DidDhtMethod.generateJwkKeyPair({ keyAlgorithm: 'secp256k1' }); expect(secp256k1KeyPair).to.exist; @@ -29,16 +36,49 @@ describe('DidDhtMethod', () => { expect(secp256k1KeyPair.publicKeyJwk.kid).to.exist; expect(secp256k1KeyPair.publicKeyJwk.alg).to.equal('ES256K'); expect(secp256k1KeyPair.publicKeyJwk.kty).to.equal('EC'); + }); + it('throws an error if an unsupported key algorithm is passed in', async () => { + await expect( + DidDhtMethod.generateJwkKeyPair({ keyAlgorithm: 'unsupported' as any }) + ).to.be.rejectedWith(Error, 'unsupported'); }); }); - describe('keysets', () => { - it('should generate a keyset with no keyset passed in', async () => { + describe('getDidIdentifierFragment()', () => { + it('should return the encoded identifier fragment for a given public key', async () => { + const testPublicKey: PublicKeyJwk = { + kty : 'OKP', + crv : 'Ed25519', + x : '9ZOlXQ7pZw7voYfQsrPPzvd1dA4ktXB5VbD1PWvl_jg', + ext : 'true', + 'key_ops' : ['verify'] + }; + + const result = await DidDhtMethod.getDidIdentifierFragment({ key: testPublicKey }); + + expect(result).to.equal('6sj4kzeq7fuo757bo9emfc6x355zk7yqr14zy6kisd4u449f9ahy'); + }); + }); + + describe('resolve()', () => { + it(`should return 'internalError' if DHT request throws error`, async () => { + const dhtDidResolutionStub = sinon.stub(DidDht, 'getDidDocument').rejects(new Error('Invalid SignedPacket bytes length, expected at least 72 bytes but got: 25')); + + const didResolutionResult = await DidDhtMethod.resolve({ didUrl: 'did:dht:h4d3ixkwt6q5a455tucw7j14jmqyghdtbr6cpiz6on5oxj5bpr3o' }); + const didResolutionMetadata = didResolutionResult.didResolutionMetadata; + expect(didResolutionMetadata.error).to.equal('internalError'); + + expect(dhtDidResolutionStub.calledOnce).to.be.true; + sinon.restore(); + }); + }); + + describe('key sets', () => { + it('should generate a key set with the identity key if no keys are passed in', async () => { const keySet = await DidDhtMethod.generateKeySet(); expect(keySet).to.exist; - expect(keySet).to.have.property('identityKey'); expect(keySet).to.have.property('verificationMethodKeys'); expect(keySet).to.not.have.property('recoveryKey'); expect(keySet).to.not.have.property('updateKey'); @@ -47,29 +87,26 @@ describe('DidDhtMethod', () => { expect(keySet.verificationMethodKeys[0].publicKeyJwk.kid).to.equal('0'); }); - it('should generate a keyset with an identity keyset passed in (wrong kid)', async () => { - const ed25519KeyPair = await DidDhtMethod.generateJwkKeyPair({ keyAlgorithm: 'Ed25519' }); - - await expect(DidDhtMethod.generateKeySet({ - keySet: { identityKey: ed25519KeyPair } - })).to.eventually.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 () => { + it('should return the key set unmodified if only the identity key is passed in', async () => { const ed25519KeyPair = await DidDhtMethod.generateJwkKeyPair({ keyId: '0', keyAlgorithm: 'Ed25519' }); - const keySet = await DidDhtMethod.generateKeySet({ keySet: { identityKey: ed25519KeyPair } }); + const identityKey: DidKeySetVerificationMethodKey = { + publicKeyJwk : ed25519KeyPair.publicKeyJwk, + privateKeyJwk : ed25519KeyPair.privateKeyJwk, + relationships : ['authentication', 'assertionMethod', 'capabilityInvocation', 'capabilityDelegation'] + }; + + const keySet = await DidDhtMethod.generateKeySet({ keySet: { verificationMethodKeys: [identityKey] } }); expect(keySet).to.exist; - expect(keySet).to.have.property('identityKey'); expect(keySet).to.have.property('verificationMethodKeys'); expect(keySet).to.not.have.property('recoveryKey'); expect(keySet).to.not.have.property('updateKey'); expect(keySet).to.not.have.property('signingKey'); expect(keySet.verificationMethodKeys).to.have.lengthOf(1); - expect(keySet.verificationMethodKeys[0].publicKeyJwk.kid).to.equal('0'); + expect(keySet.verificationMethodKeys[0]).to.deep.equal(identityKey); }); - it('should generate a keyset with a non identity keyset passed in', async () => { + it('should generate the identity key if non-identity keys are passed in', async () => { const ed25519KeyPair = await DidDhtMethod.generateJwkKeyPair({ keyAlgorithm: 'Ed25519' }); const vm: DidKeySetVerificationMethodKey = { publicKeyJwk : ed25519KeyPair.publicKeyJwk, @@ -80,7 +117,6 @@ describe('DidDhtMethod', () => { const keySet = await DidDhtMethod.generateKeySet({ keySet: { verificationMethodKeys: [vm] } }); expect(keySet).to.exist; - expect(keySet).to.have.property('identityKey'); expect(keySet).to.have.property('verificationMethodKeys'); expect(keySet).to.not.have.property('recoveryKey'); expect(keySet).to.not.have.property('updateKey'); @@ -93,10 +129,32 @@ describe('DidDhtMethod', () => { expect(keySet.verificationMethodKeys[1].publicKeyJwk.kid).to.equal('0'); } }); + + it('should generate key ID values for provided keys, if missing', async () => { + const ed25519KeyPair = await DidDhtMethod.generateJwkKeyPair({ keyAlgorithm: 'Ed25519' }); + + // Remove the kid values from the key pair. + delete ed25519KeyPair.publicKeyJwk.kid; + delete ed25519KeyPair.privateKeyJwk.kid; + + const vm: DidKeySetVerificationMethodKey = { + publicKeyJwk : ed25519KeyPair.publicKeyJwk, + privateKeyJwk : ed25519KeyPair.privateKeyJwk, + relationships : ['authentication', 'assertionMethod', 'capabilityInvocation', 'capabilityDelegation'] + }; + + const keySet = await DidDhtMethod.generateKeySet({ keySet: { verificationMethodKeys: [vm] } }); + + // Verify that the key ID values were generated. + expect(keySet.verificationMethodKeys[0].publicKeyJwk.kid).to.exist; + expect(keySet.verificationMethodKeys[0].privateKeyJwk.kid).to.exist; + expect(keySet.verificationMethodKeys[1].publicKeyJwk.kid).to.exist; + expect(keySet.verificationMethodKeys[1].privateKeyJwk.kid).to.exist; + }); }); - describe('dids', () => { - it('should generate a did identifier given a public key jwk', async () => { + 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 }); @@ -104,7 +162,7 @@ describe('DidDhtMethod', () => { expect(did).to.contain('did:dht:'); }); - it('should create a did document without options', async () => { + it('should create a DID document without options', async () => { const { document, keySet } = await DidDhtMethod.create(); expect(document).to.exist; @@ -127,21 +185,23 @@ describe('DidDhtMethod', () => { const ks = keySet as DidDhtKeySet; expect(ks).to.exist; - expect(ks.identityKey).to.exist; - expect(ks.identityKey.publicKeyJwk).to.exist; - expect(ks.identityKey.privateKeyJwk).to.exist; - expect(ks.identityKey.publicKeyJwk.kid).to.equal('0'); + const identityKey = keySet.verificationMethodKeys.find(key => key.publicKeyJwk.kid === '0'); + expect(identityKey).to.exist; + expect(identityKey.publicKeyJwk).to.exist; + expect(identityKey.privateKeyJwk).to.exist; + expect(identityKey.publicKeyJwk.kid).to.equal('0'); }); - it('should create a did document with a non identity key option', async () => { + it('should create a DID document with a non identity key option', async () => { const ed25519KeyPair = await DidDhtMethod.generateJwkKeyPair({ keyAlgorithm: 'Ed25519' }); - const vm: DidKeySetVerificationMethodKey = { - publicKeyJwk : ed25519KeyPair.publicKeyJwk, - privateKeyJwk : ed25519KeyPair.privateKeyJwk, - relationships : ['authentication', 'assertionMethod', 'capabilityInvocation', 'capabilityDelegation'] + const keySet: DidDhtKeySet = { + verificationMethodKeys: [{ + publicKeyJwk : ed25519KeyPair.publicKeyJwk, + privateKeyJwk : ed25519KeyPair.privateKeyJwk, + relationships : ['authentication', 'assertionMethod', 'capabilityInvocation', 'capabilityDelegation'] + }] }; - const keySet = await DidDhtMethod.generateKeySet({ keySet: { verificationMethodKeys: [vm] }}); const { document } = await DidDhtMethod.create({ keySet }); expect(document).to.exist; @@ -163,13 +223,14 @@ describe('DidDhtMethod', () => { expect(document.capabilityInvocation[1]).to.equal(`#0`); expect(keySet).to.exist; - expect(keySet.identityKey).to.exist; - expect(keySet.identityKey.publicKeyJwk).to.exist; - expect(keySet.identityKey.privateKeyJwk).to.exist; - expect(keySet.identityKey.publicKeyJwk.kid).to.equal('0'); + const identityKey = keySet.verificationMethodKeys.find(key => key.publicKeyJwk.kid === '0'); + expect(identityKey).to.exist; + expect(identityKey.publicKeyJwk).to.exist; + expect(identityKey.privateKeyJwk).to.exist; + expect(identityKey.publicKeyJwk.kid).to.equal('0'); }); - it('should create a did document with services', async () => { + it('should create a DID document with services', async () => { const services: DidService[] = [{ id : 'agentId', type : 'agent', @@ -199,35 +260,39 @@ describe('DidDhtMethod', () => { }); }); - describe('did publishing and resolving', function () { - it('should publish and get a did document', async () => { + describe('DID publishing and resolving', function () { + it('should publish and DID should be resolvable', async () => { const { document, keySet } = await DidDhtMethod.create(); + const identityKey = keySet.verificationMethodKeys.find(key => key.publicKeyJwk.kid === '0'); - const dhtDidPublishSpy = sinon.stub(DidDhtMethod, 'publish').resolves(true); - const dhtDidResolutionSpy = sinon.stub(DidDhtMethod, 'resolve').resolves({ + const dhtDidPublishStub = sinon.stub(DidDht, 'publishDidDocument').resolves(true); + const dhtDidResolutionStub = sinon.stub(DidDhtMethod, 'resolve').resolves({ '@context' : 'https://w3id.org/did-resolution/v1', didDocument : document, didDocumentMetadata : {}, didResolutionMetadata : { - contentType : 'application/did+ld+json', - error : 'invalidDid', - errorMessage : `Cannot parse DID: ${document.id}` + contentType : 'application/did+ld+json', + did : { + didString : document.id, + methodSpecificId : parseDid({ didUrl: document.id }).id, + method : 'dht' + } } }); - const isPublished = await DidDhtMethod.publish({ keySet, didDocument: document }); + const isPublished = await DidDhtMethod.publish({ identityKey, didDocument: document }); expect(isPublished).to.be.true; const didResolutionResult = await DidDhtMethod.resolve({ didUrl: document.id }); const didDocument = didResolutionResult.didDocument; expect(didDocument.id).to.deep.equal(document.id); - sinon.assert.calledOnce(dhtDidPublishSpy); - sinon.assert.calledOnce(dhtDidResolutionSpy); + expect(dhtDidPublishStub.calledOnce).to.be.true; + expect(dhtDidResolutionStub.calledOnce).to.be.true; sinon.restore(); }); - it('should create with publish and get a did document', async () => { + it('should create with publish and return a DID document', async () => { const mockDocument: PortableDid = { keySet : 'any' as any, did : 'did:dht:123456789abcdefghi', @@ -250,19 +315,22 @@ describe('DidDhtMethod', () => { capabilityInvocation : ['did:dht:123456789abcdefghi#0'] } }; - const didDhtCreateSpy = sinon.stub(DidDhtMethod, 'create').resolves(mockDocument); + const didDhtCreateStub = sinon.stub(DidDhtMethod, 'create').resolves(mockDocument); const { document } = await DidDhtMethod.create({ publish: true }); const did = document.id; - const dhtDidResolutionSpy = sinon.stub(DidDhtMethod, 'resolve').resolves({ + const dhtDidResolutionStub = sinon.stub(DidDhtMethod, 'resolve').resolves({ '@context' : 'https://w3id.org/did-resolution/v1', didDocument : document, didDocumentMetadata : {}, didResolutionMetadata : { - contentType : 'application/did+ld+json', - error : 'invalidDid', - errorMessage : `Cannot parse DID: ${document.id}` + contentType : 'application/did+ld+json', + did : { + didString : 'did:dht:123456789abcdefgh', + methodSpecificId : '123456789abcdefgh', + method : 'dht' + } } }); @@ -275,9 +343,173 @@ describe('DidDhtMethod', () => { 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); - sinon.assert.calledOnce(didDhtCreateSpy); - sinon.assert.calledOnce(dhtDidResolutionSpy); + expect(didDhtCreateStub.calledOnce).to.be.true; + expect(dhtDidResolutionStub.calledOnce).to.be.true; + sinon.restore(); + }); + + it('should create with publish and DID should be resolvable', async () => { + const keySet: DidDhtKeySet = { + verificationMethodKeys: [{ + 'privateKeyJwk': { + 'd' : '2dPyiFL-vd21lxLKoyylz1nEK5EMByABqB2Fqio76sU', + 'alg' : 'EdDSA', + 'crv' : 'Ed25519', + 'kty' : 'OKP', + 'ext' : 'true', + 'key_ops' : [ + 'sign' + ], + 'x' : '5oeavVSPnbxre4zZTqZaStwDcHEJPMbW_oC3B6dhaTM', + 'kid' : '0' + }, + 'publicKeyJwk': { + 'alg' : 'EdDSA', + 'crv' : 'Ed25519', + 'kty' : 'OKP', + 'ext' : 'true', + 'key_ops' : [ + 'verify' + ], + 'x' : '5oeavVSPnbxre4zZTqZaStwDcHEJPMbW_oC3B6dhaTM', + 'kid' : '0' + }, + 'relationships': [ + 'authentication', + 'assertionMethod', + 'capabilityInvocation', + 'capabilityDelegation' + ] + }] + }; + + const didDocument: DidDocument = { + 'id' : 'did:dht:h4d3ixkwt6q5a455tucw7j14jmqyghdtbr6cpiz6on5oxj5bpr3o', + 'verificationMethod' : [ + { + 'id' : 'did:dht:h4d3ixkwt6q5a455tucw7j14jmqyghdtbr6cpiz6on5oxj5bpr3o#0', + 'type' : 'JsonWebKey2020', + 'controller' : 'did:dht:h4d3ixkwt6q5a455tucw7j14jmqyghdtbr6cpiz6on5oxj5bpr3o', + 'publicKeyJwk' : { + 'alg' : 'EdDSA', + 'crv' : 'Ed25519', + 'kty' : 'OKP', + 'ext' : 'true', + 'key_ops' : [ + 'verify' + ], + 'x' : '5oeavVSPnbxre4zZTqZaStwDcHEJPMbW_oC3B6dhaTM', + 'kid' : '0' + } + } + ], + 'authentication': [ + '#0' + ], + 'assertionMethod': [ + '#0' + ], + 'capabilityInvocation': [ + '#0' + ], + 'capabilityDelegation': [ + '#0' + ] + }; + + const dhtDidPublishStub = sinon.stub(DidDhtMethod, 'publish').resolves(true); + const dhtDidResolutionStub = sinon.stub(DidDhtMethod, 'resolve').resolves({ + '@context' : 'https://w3id.org/did-resolution/v1', + didDocument, + didDocumentMetadata : {}, + didResolutionMetadata : { + contentType : 'application/did+ld+json', + did : { + didString : 'did:dht:h4d3ixkwt6q5a455tucw7j14jmqyghdtbr6cpiz6on5oxj5bpr3o', + methodSpecificId : 'h4d3ixkwt6q5a455tucw7j14jmqyghdtbr6cpiz6on5oxj5bpr3o', + method : 'dht' + } + } + }); + + const portableDid = await DidDhtMethod.create({ publish: true, keySet: keySet }); + expect(portableDid).to.exist; + expect(portableDid.did).to.exist; + expect(portableDid.document).to.exist; + expect(portableDid.keySet).to.exist; + expect(portableDid.document.id).to.deep.equal(didDocument.id); + + const didResolutionResult = await DidDhtMethod.resolve({ didUrl: didDocument.id }); + expect(didDocument.id).to.deep.equal(didResolutionResult.didDocument.id); + + expect(dhtDidPublishStub.calledOnce).to.be.true; + expect(dhtDidResolutionStub.calledOnce).to.be.true; sinon.restore(); }); }); -}); + + describe('Integration with DidResolver', () => { + it('DidResolver resolves a did:dht DID', async () => { + // Previously published DID. + const did = 'did:dht:h4d3ixkwt6q5a455tucw7j14jmqyghdtbr6cpiz6on5oxj5bpr3o'; + const didDocument: DidDocument = { + 'id' : 'did:dht:h4d3ixkwt6q5a455tucw7j14jmqyghdtbr6cpiz6on5oxj5bpr3o', + 'verificationMethod' : [ + { + 'id' : 'did:dht:h4d3ixkwt6q5a455tucw7j14jmqyghdtbr6cpiz6on5oxj5bpr3o#0', + 'type' : 'JsonWebKey2020', + 'controller' : 'did:dht:h4d3ixkwt6q5a455tucw7j14jmqyghdtbr6cpiz6on5oxj5bpr3o', + 'publicKeyJwk' : { + 'alg' : 'EdDSA', + 'crv' : 'Ed25519', + 'kty' : 'OKP', + 'ext' : 'true', + 'key_ops' : [ + 'verify' + ], + 'x' : '5oeavVSPnbxre4zZTqZaStwDcHEJPMbW_oC3B6dhaTM', + 'kid' : '0' + } + } + ], + 'authentication': [ + '#0' + ], + 'assertionMethod': [ + '#0' + ], + 'capabilityInvocation': [ + '#0' + ], + 'capabilityDelegation': [ + '#0' + ] + }; + + const dhtDidResolutionStub = sinon.stub(DidDht, 'getDidDocument').resolves(didDocument); + + // Instantiate a DidResolver with the DidJwkMethod. + const didResolver = new DidResolver({ didResolvers: [DidDhtMethod] }); + + // Resolve the DID using the DidResolver. + const { didDocument: resolvedDocument } = await didResolver.resolve(did); + + // Verify that the resolved document matches the created document. + expect(resolvedDocument).to.deep.equal(didDocument); + + expect(dhtDidResolutionStub.calledOnce).to.be.true; + sinon.restore(); + }); + + it('returns an error for invalid didUrl', async () => { + const result = await DidDhtMethod.resolve({ didUrl: 'invalid' }); + expect(result).to.have.property('didResolutionMetadata').which.has.property('error', 'invalidDid'); + }); + + it('returns an error for unsupported method', async () => { + const result = await DidDhtMethod.resolve({ didUrl: 'did:unsupported:xyz' }); + expect(result).to.have.property('didResolutionMetadata').which.has.property('error', 'methodNotSupported'); + }); + }); + +}); \ No newline at end of file From a23a7e600868f585ac40624c6b70a35183f626c2 Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Tue, 5 Dec 2023 14:44:44 -0500 Subject: [PATCH 2/3] Update @web5/credentials with DID DHT key set changes Signed-off-by: Frank Hinek --- .../tests/verifiable-credential.spec.ts | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/packages/credentials/tests/verifiable-credential.spec.ts b/packages/credentials/tests/verifiable-credential.spec.ts index 50d28a14a..79ffebf9a 100644 --- a/packages/credentials/tests/verifiable-credential.spec.ts +++ b/packages/credentials/tests/verifiable-credential.spec.ts @@ -185,31 +185,6 @@ describe('Verifiable Credential Tests', () => { it('verify does not throw an exception with vaild vc signed by did:dht', async () => { const mockDocument: PortableDid = { keySet: { - identityKey: { - privateKeyJwk: { - d : '_8gihSI-m8aOCCM6jHg33d8kxdImPBN4C5_bZIu10XU', - alg : 'EdDSA', - crv : 'Ed25519', - kty : 'OKP', - ext : 'true', - key_ops : [ - 'sign' - ], - x : 'Qm88q6jAN9tfnrLt5V2zAiZs7wD_jnewHp7HIvM3dGo', - kid : '0' - }, - publicKeyJwk: { - alg : 'EdDSA', - crv : 'Ed25519', - kty : 'OKP', - ext : 'true', - key_ops : [ - 'verify' - ], - x : 'Qm88q6jAN9tfnrLt5V2zAiZs7wD_jnewHp7HIvM3dGo', - kid : '0' - } - }, verificationMethodKeys: [ { privateKeyJwk: { From ac3737fd3d11540b10e4d2dbe0db8c3e4d686923 Mon Sep 17 00:00:00 2001 From: Frank Hinek Date: Tue, 5 Dec 2023 21:46:49 -0500 Subject: [PATCH 3/3] Address request to specify did+json DID Resolution Result Signed-off-by: Frank Hinek --- packages/dids/src/did-dht.ts | 8 ++++---- packages/dids/tests/did-dht.spec.ts | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/dids/src/did-dht.ts b/packages/dids/src/did-dht.ts index 3d6ad7aef..06a5a7489 100644 --- a/packages/dids/src/did-dht.ts +++ b/packages/dids/src/did-dht.ts @@ -258,7 +258,7 @@ export class DidDhtMethod implements DidMethod { didDocument : null, didDocumentMetadata : {}, didResolutionMetadata : { - contentType : 'application/did+ld+json', + contentType : 'application/did+json', error : 'invalidDid', errorMessage : `Cannot parse DID: ${didUrl}` } @@ -271,7 +271,7 @@ export class DidDhtMethod implements DidMethod { didDocument : null, didDocumentMetadata : {}, didResolutionMetadata : { - contentType : 'application/did+ld+json', + contentType : 'application/did+json', error : 'methodNotSupported', errorMessage : `Method not supported: ${parsedDid.method}` } @@ -294,7 +294,7 @@ export class DidDhtMethod implements DidMethod { didDocument : null, didDocumentMetadata : {}, didResolutionMetadata : { - contentType : 'application/did+ld+json', + contentType : 'application/did+json', error : 'internalError', errorMessage : `An unexpected error occurred while resolving DID: ${parsedDid.did}` } @@ -306,7 +306,7 @@ export class DidDhtMethod implements DidMethod { didDocument, didDocumentMetadata : {}, didResolutionMetadata : { - contentType : 'application/did+ld+json', + contentType : 'application/did+json', did : { didString : parsedDid.did, methodSpecificId : parsedDid.id, diff --git a/packages/dids/tests/did-dht.spec.ts b/packages/dids/tests/did-dht.spec.ts index 507ae4e1f..e0fb1e3a1 100644 --- a/packages/dids/tests/did-dht.spec.ts +++ b/packages/dids/tests/did-dht.spec.ts @@ -271,7 +271,7 @@ describe('DidDhtMethod', () => { didDocument : document, didDocumentMetadata : {}, didResolutionMetadata : { - contentType : 'application/did+ld+json', + contentType : 'application/did+json', did : { didString : document.id, methodSpecificId : parseDid({ didUrl: document.id }).id, @@ -325,7 +325,7 @@ describe('DidDhtMethod', () => { didDocument : document, didDocumentMetadata : {}, didResolutionMetadata : { - contentType : 'application/did+ld+json', + contentType : 'application/did+json', did : { didString : 'did:dht:123456789abcdefgh', methodSpecificId : '123456789abcdefgh', @@ -423,7 +423,7 @@ describe('DidDhtMethod', () => { didDocument, didDocumentMetadata : {}, didResolutionMetadata : { - contentType : 'application/did+ld+json', + contentType : 'application/did+json', did : { didString : 'did:dht:h4d3ixkwt6q5a455tucw7j14jmqyghdtbr6cpiz6on5oxj5bpr3o', methodSpecificId : 'h4d3ixkwt6q5a455tucw7j14jmqyghdtbr6cpiz6on5oxj5bpr3o',