Skip to content

Commit

Permalink
DID DHT Enhancements (#334)
Browse files Browse the repository at this point in the history
This PR primarily addresses Issue #312 by improving the error handling when attempting to resolve a did:dht DID that hasn't yet been published. Additionally, it modifies the key set handling to better align with the original intent and other existing DID method implementations. This was necessary to support the forthcoming switch to did:dht as the default DID for @web5/api package. Lastly it approves test coverage for the DID DHT related code.
  • Loading branch information
frankhinek authored Dec 6, 2023
2 parents 5e08aef + ac3737f commit 8fe94d0
Show file tree
Hide file tree
Showing 4 changed files with 359 additions and 138 deletions.
31 changes: 3 additions & 28 deletions packages/credentials/tests/verifiable-credential.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -276,7 +251,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 });

Expand Down Expand Up @@ -343,8 +318,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();
});
});
Expand Down
99 changes: 57 additions & 42 deletions packages/dids/src/did-dht.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ export type DidDhtCreateOptions = {
}

export type DidDhtKeySet = {
identityKey?: JwkKeyPair;
verificationMethodKeys?: DidKeySetVerificationMethodKey[];
}

Expand All @@ -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<PortableDid> {
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<Record<VerificationRelationship, string[]>> = {};
Expand All @@ -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,
Expand Down Expand Up @@ -156,35 +160,25 @@ export class DidDhtMethod implements DidMethod {
}): Promise<DidDhtKeySet> {
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;
Expand All @@ -198,9 +192,9 @@ export class DidDhtMethod implements DidMethod {
public static async getDidIdentifier(options: {
key: PublicKeyJwk
}): Promise<string> {
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;
}
Expand All @@ -213,8 +207,8 @@ export class DidDhtMethod implements DidMethod {
public static async getDidIdentifierFragment(options: {
key: PublicKeyJwk
}): Promise<string> {
const {key} = options;
const cryptoKey = await Jose.jwkToCryptoKey({key});
const { key } = options;
const cryptoKey = await Jose.jwkToCryptoKey({ key });
return z32.encode(cryptoKey.material);
}

Expand All @@ -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<boolean> {
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: {
Expand Down Expand Up @@ -261,10 +255,10 @@ 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',
contentType : 'application/did+json',
error : 'invalidDid',
errorMessage : `Cannot parse DID: ${didUrl}`
}
Expand All @@ -274,24 +268,45 @@ 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',
contentType : 'application/did+json',
error : 'methodNotSupported',
errorMessage : `Method not supported: ${parsedDid.method}`
}
};
}

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+json',
error : 'internalError',
errorMessage : `An unexpected error occurred while resolving DID: ${parsedDid.did}`
}
};
}

return {
'@context' : 'https://w3id.org/did-resolution/v1',
didDocument,
didDocumentMetadata : {},
didResolutionMetadata : {
contentType : 'application/did+ld+json',
contentType : 'application/did+json',
did : {
didString : parsedDid.did,
methodSpecificId : parsedDid.id,
Expand Down
19 changes: 9 additions & 10 deletions packages/dids/tests/dht.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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: {
Expand All @@ -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();
});

Expand Down Expand Up @@ -85,4 +84,4 @@ describe('DidDht', () => {
expect(document.verificationMethod[1].publicKeyJwk.kty).to.deep.equal(decoded.verificationMethod[1].publicKeyJwk.kty);
});
});
});
});
Loading

0 comments on commit 8fe94d0

Please sign in to comment.