Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DID DHT Enhancements #334

Merged
merged 3 commits into from
Dec 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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