Skip to content

Commit

Permalink
DID DHT Vector 3 Compliance (#636)
Browse files Browse the repository at this point in the history
1. Vector 3 compliance
2. X25519 support
3. Previous DID link support
4. DNS record chunking support for record > 255 characters (only in context of vector 3 compliance, will need to apply generically in a separate PR that addresses item 4 specifically in #497)
5. Some test refactoring
thehenrytsai authored May 24, 2024
1 parent 269384b commit b425bbc
Showing 12 changed files with 292 additions and 50 deletions.
8 changes: 8 additions & 0 deletions .changeset/modern-cobras-serve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@web5/dids": minor
---

1. Vector 3 compliance
2. X25519 support
3. Previous DID link support
4. DNS record chunking support for record > 255 characters (only in context of vector 3 compliance)
3 changes: 3 additions & 0 deletions packages/api/src/web-features.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@

declare const ServiceWorkerGlobalScope: any;

/**
* Installs the DWeb networking features in the current environment.
*/
export function installNetworkingFeatures(path: string): void {
const workerSelf = self as any;

4 changes: 3 additions & 1 deletion packages/dids/package.json
Original file line number Diff line number Diff line change
@@ -89,7 +89,8 @@
"devDependencies": {
"@playwright/test": "1.40.1",
"@types/bencode": "2.0.4",
"@types/chai": "4.3.6",
"@types/chai": "4.3.16",
"@types/chai-as-promised": "7.1.8",
"@types/eslint": "8.56.10",
"@types/mocha": "10.0.6",
"@types/ms": "0.7.34",
@@ -101,6 +102,7 @@
"@web/test-runner-playwright": "0.11.0",
"c8": "9.1.0",
"chai": "5.1.1",
"chai-as-promised": "7.1.2",
"esbuild": "0.19.8",
"eslint": "9.3.0",
"eslint-plugin-mocha": "10.4.3",
5 changes: 4 additions & 1 deletion packages/dids/src/did-error.ts
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@ export class DidError extends Error {
* @param message - A human-readable description of the error.
*/
constructor(public code: DidErrorCode, message: string) {
super(message);
super(`${code}: ${message}`);
this.name = 'DidError';

// Ensures that instanceof works properly, the correct prototype chain when using inheritance,
@@ -46,6 +46,9 @@ export enum DidErrorCode {
/** The DID URL supplied to the dereferencing function does not conform to valid syntax. */
InvalidDidUrl = 'invalidDidUrl',

/** The given proof of a previous DID is invalid */
InvalidPreviousDidProof = 'invalidPreviousDidProof',

/** An invalid public key is detected during a DID operation. */
InvalidPublicKey = 'invalidPublicKey',

101 changes: 92 additions & 9 deletions packages/dids/src/methods/did-dht.ts
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ import type {

import bencode from 'bencode';
import { Convert } from '@web5/common';
import { computeJwkThumbprint, Ed25519, LocalKeyManager, Secp256k1, Secp256r1 } from '@web5/crypto';
import { computeJwkThumbprint, Ed25519, LocalKeyManager, Secp256k1, Secp256r1, X25519 } from '@web5/crypto';
import { AUTHORITATIVE_ANSWER, decode as dnsPacketDecode, encode as dnsPacketEncode } from '@dnsquery/dns-packet';

import type { DidMetadata, PortableDid } from '../types/portable-did.js';
@@ -206,6 +206,17 @@ export interface DidDhtCreateOptions<TKms> extends DidCreateOptions<TKms> {
verificationMethods?: DidCreateVerificationMethod<TKms>[];
}

/**
* Proof to used to construct the `_prv._did.` DNS record as described in https://did-dht.com/#rotation to link a DID to a previous DID.
*/
export type PreviousDidProof = {
/** The previous DID. */
previousDid: string;

/** The signature signed using the private Identity Key of the previous DID in Base64URL format. */
signature: string;
};

/**
* The default DID DHT Gateway or Pkarr Relay server to use when publishing and resolving DID
* documents.
@@ -332,7 +343,7 @@ export enum DidDhtRegisteredKeyType {
* Ed25519: A public-key signature system using the EdDSA (Edwards-curve Digital Signature
* Algorithm) and Curve25519.
*/
Ed25519 = 0,
Ed25519 = 0,

/**
* secp256k1: A cryptographic curve used for digital signatures in a range of decentralized
@@ -344,7 +355,12 @@ export enum DidDhtRegisteredKeyType {
* secp256r1: Also known as P-256 or prime256v1, this curve is used for cryptographic operations
* and is widely supported in various cryptographic libraries and standards.
*/
secp256r1 = 2
secp256r1 = 2,

/**
* X25519: A public key used for Diffie-Hellman key exchange using Curve25519.
*/
X25519 = 3,
}

/**
@@ -391,7 +407,8 @@ const AlgorithmToKeyTypeMap = {
ES256 : DidDhtRegisteredKeyType.secp256r1,
'P-256' : DidDhtRegisteredKeyType.secp256r1,
secp256k1 : DidDhtRegisteredKeyType.secp256k1,
secp256r1 : DidDhtRegisteredKeyType.secp256r1
secp256r1 : DidDhtRegisteredKeyType.secp256r1,
X25519 : DidDhtRegisteredKeyType.X25519,
} as const;

/**
@@ -401,6 +418,7 @@ const KeyTypeToDefaultAlgorithmMap = {
[DidDhtRegisteredKeyType.Ed25519] : 'Ed25519',
[DidDhtRegisteredKeyType.secp256k1] : 'ES256K',
[DidDhtRegisteredKeyType.secp256r1] : 'ES256',
[DidDhtRegisteredKeyType.X25519] : 'ECDH-ES+A256KW',
};

/**
@@ -1068,8 +1086,10 @@ export class DidDhtDocument {
// other properties from the decoded TXT record data.
const { id, t, se, ...customProperties } = DidDhtUtils.parseTxtDataToObject(answer.data);

// The service endpoint can either be a string or an array of strings.
const serviceEndpoint = se.includes(VALUE_SEPARATOR) ? se.split(VALUE_SEPARATOR) : se;
// if multi-values: 'a,b,c' -> ['a', 'b', 'c'], if single-value: 'a' -> ['a']
// NOTE: The service endpoint technically can either be a string or an array of strings,
// we enforce an array for single-value to simplify verification of vector 3 in the spec: https://did-dht.com/#vector-3
const serviceEndpoint = se.includes(VALUE_SEPARATOR) ? se.split(VALUE_SEPARATOR) : [se];

// Convert custom property values to either a string or an array of strings.
const serviceProperties = Object.fromEntries(Object.entries(customProperties).map(
@@ -1135,19 +1155,38 @@ export class DidDhtDocument {
* @param params.didDocument - The DID document to convert to a DNS packet.
* @param params.didMetadata - The DID metadata to include in the DNS packet.
* @param params.authoritativeGatewayUris - The URIs of the Authoritative Gateways to generate NS records from.
* @param params.previousDidProof - The signature proof that this DID is linked to the given previous DID.
* @returns A promise that resolves to a DNS packet.
*/
public static async toDnsPacket({ didDocument, didMetadata, authoritativeGatewayUris }: {
public static async toDnsPacket({ didDocument, didMetadata, authoritativeGatewayUris, previousDidProof }: {
didDocument: DidDocument;
didMetadata: DidMetadata;
authoritativeGatewayUris?: string[];
previousDidProof?: PreviousDidProof;
}): Promise<Packet> {
const txtRecords: TxtAnswer[] = [];
const nsRecords: StringAnswer[] = [];
const idLookup = new Map<string, string>();
const serviceIds: string[] = [];
const verificationMethodIds: string[] = [];

// Add `_prv._did.` TXT record if previous DID proof is provided and valid.
if (previousDidProof !== undefined) {
const { signature, previousDid } = previousDidProof;

await DidDhtUtils.validatePreviousDidProof({
newDid: didDocument.id,
previousDidProof
});

txtRecords.push({
type : 'TXT',
name : '_prv._did.',
ttl : DNS_RECORD_TTL,
data : `id=${previousDid};s=${signature}`
});
}

// Add DNS TXT records if the DID document contains an `alsoKnownAs` property.
if (didDocument.alsoKnownAs) {
txtRecords.push({
@@ -1231,12 +1270,15 @@ export class DidDhtDocument {
([key, value]) => `${key}=${value}`
);

const txtDataString = txtData.join(PROPERTY_SEPARATOR);
const data = DidDhtUtils.chunkDataIfNeeded(txtDataString);

// Add a TXT record for the verification method.
txtRecords.push({
type : 'TXT',
name : `_${dnsRecordId}._did.`,
ttl : DNS_RECORD_TTL,
data : txtData.join(PROPERTY_SEPARATOR)
data
});
});

@@ -1475,7 +1517,8 @@ export class DidDhtUtils {
bytesToPublicKey : Secp256k1.bytesToPublicKey,
privateKeyToBytes : Secp256k1.privateKeyToBytes,
bytesToPrivateKey : Secp256k1.bytesToPrivateKey,
}
},
X25519: X25519,
};

const converter = converters[curve];
@@ -1546,4 +1589,44 @@ export class DidDhtUtils {
throw new DidError(DidErrorCode.InternalError, 'Pkarr returned DNS TXT record with invalid data type');
}
}

/**
* Validates the proof of previous DID given.
*
* @param params - The parameters to validate the previous DID proof.
* @param params.newDid - The new DID that the previous DID is linking to.
* @param params.previousDidProof - The proof of the previous DID, containing the previous DID and signature signed by the previous DID.
*/
public static async validatePreviousDidProof({ newDid, previousDidProof }: {
newDid: string,
previousDidProof: PreviousDidProof,
}): Promise<void> {
const key = await DidDhtUtils.identifierToIdentityKey({ didUri: previousDidProof.previousDid });
const data = DidDhtUtils.identifierToIdentityKeyBytes({ didUri: newDid });
const signature = Convert.base64Url(previousDidProof.signature).toUint8Array();
const isValid = await Ed25519.verify({ key, data, signature });

if (!isValid) {
throw new DidError(DidErrorCode.InvalidPreviousDidProof, 'The previous DID proof is invalid.');
}
}

/**
* Splits a string into chunks of length 255 if the string exceeds length 255.
* @param data - The string to split into chunks.
* @returns The original string if its length is less than or equal to 255, otherwise an array of chunked strings.
*/
public static chunkDataIfNeeded(data: string): string | string[] {
if (data.length <= 255) {
return data;
}

// Split the data into chunks of 255 characters.
const chunks: string[] = [];
for (let i = 0; i < data.length; i += 255) {
chunks.push(data.slice(i, i + 255)); // end index is ignored if it exceeds the length of the string
}

return chunks;
}
}
Original file line number Diff line number Diff line change
@@ -52,6 +52,9 @@
}
]
},
"authoritativeGatewayUris": [
"gateway1.example-did-dht-gateway.com"
],
"dnsRecords": [
{
"name": "_did.cyuoqaf7itop8ohww4yn5ojg13qaq83r9zihgqntc5i9zwrfdfoo.",
108 changes: 108 additions & 0 deletions packages/dids/tests/fixtures/test-vectors/did-dht/vector-3.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
{
"didDocument": {
"id": "did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy",
"verificationMethod": [
{
"id": "did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy#0",
"type": "JsonWebKey",
"controller": "did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy",
"publicKeyJwk": {
"kid": "0",
"alg": "Ed25519",
"crv": "Ed25519",
"kty": "OKP",
"x": "sTyTLYw-n1NI9X-84NaCuis1wZjAA8lku6f6Et5201g"
}
},
{
"id": "did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy#WVy5IWMa36AoyAXZDvPd5j9zxt2t-GjifDEV-DwgIdQ",
"type": "JsonWebKey",
"controller": "did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy",
"publicKeyJwk": {
"kid": "WVy5IWMa36AoyAXZDvPd5j9zxt2t-GjifDEV-DwgIdQ",
"alg": "ECDH-ES+A128KW",
"crv": "X25519",
"kty": "OKP",
"x": "3POE0_i2mGeZ2qiQCA3KcLfi1fZo0311CXFSIwt1nB4"
}
}
],
"authentication": [
"did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy#0"
],
"assertionMethod": [
"did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy#0"
],
"keyAgreement": [
"did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy#WVy5IWMa36AoyAXZDvPd5j9zxt2t-GjifDEV-DwgIdQ"
],
"capabilityInvocation": [
"did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy#0"
],
"capabilityDelegation": [
"did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy#0"
],
"service": [
{
"id": "did:dht:sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy#service-1",
"type": "TestLongService",
"serviceEndpoint": ["https://test-lllllllllllllllllllllllllllllllllllooooooooooooooooooooonnnnnnnnnnnnnnnnnnngggggggggggggggggggggggggggggggggggggsssssssssssssssssssssssssseeeeeeeeeeeeeeeeeeerrrrrrrrrrrrrrrvvvvvvvvvvvvvvvvvvvviiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiccccccccccccccccccccccccccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee.com/1"]
}
]
},
"authoritativeGatewayUris": [
"gateway1.example-did-dht-gateway.com",
"gateway2.example-did-dht-gateway.com"
],
"previousDidProof": {
"previousDid": "did:dht:x3heus3ke8fhgb5pbecday9wtbfynd6m19q4pm6gcf5j356qhjzo",
"signature": "Tt9DRT6J32v7O2lzbfasW63_FfagiMHTHxtaEOD7p85zHE0r_EfiNleyL6BZGyB1P-oQ5p6_7KONaHAjr2K6Bw"
},
"dnsRecords": [
{
"name": "_prv._did.",
"type": "TXT",
"ttl": 7200,
"rdata": "id=did:dht:x3heus3ke8fhgb5pbecday9wtbfynd6m19q4pm6gcf5j356qhjzo;s=Tt9DRT6J32v7O2lzbfasW63_FfagiMHTHxtaEOD7p85zHE0r_EfiNleyL6BZGyB1P-oQ5p6_7KONaHAjr2K6Bw"
},
{
"name": "_did.sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy.",
"type": "NS",
"ttl": 7200,
"rdata": "gateway1.example-did-dht-gateway.com."
},
{
"name": "_did.sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy.",
"type": "NS",
"ttl": 7200,
"rdata": "gateway2.example-did-dht-gateway.com."
},
{
"name": "_did.sr6jgmcc84xig18ix66qbiwnzeiumocaaybh13f5w97bfzus4pcy.",
"type": "TXT",
"ttl": 7200,
"rdata": "v=0;vm=k0,k1;auth=k0;asm=k0;agm=k1;inv=k0;del=k0;svc=s0"
},
{
"name": "_k0._did.",
"type": "TXT",
"ttl": 7200,
"rdata": "t=0;k=sTyTLYw-n1NI9X-84NaCuis1wZjAA8lku6f6Et5201g"
},
{
"name": "_k1._did.",
"type": "TXT",
"ttl": 7200,
"rdata": "t=3;k=3POE0_i2mGeZ2qiQCA3KcLfi1fZo0311CXFSIwt1nB4;a=ECDH-ES+A128KW"
},
{
"name": "_s0._did.",
"type": "TXT",
"ttl": 7200,
"rdata": [
"id=service-1;t=TestLongService;se=https://test-lllllllllllllllllllllllllllllllllllooooooooooooooooooooonnnnnnnnnnnnnnnnnnngggggggggggggggggggggggggggggggggggggsssssssssssssssssssssssssseeeeeeeeeeeeeeeeeeerrrrrrrrrrrrrrrvvvvvvvvvvvvvvvvvvvviiiiiiiiiiiiiiii",
"iiiiiiiiiiiiiiiccccccccccccccccccccccccccccccceeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee.com/1"
]
}
]
}
Loading

0 comments on commit b425bbc

Please sign in to comment.