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 Vector 3 Compliance #636

Merged
merged 5 commits into from
May 24, 2024
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
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;

Expand Down
4 changes: 3 additions & 1 deletion packages/dids/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
5 changes: 4 additions & 1 deletion packages/dids/src/did-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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',

Expand Down
101 changes: 92 additions & 9 deletions packages/dids/src/methods/did-dht.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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,
}

/**
Expand Down Expand Up @@ -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;

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

/**
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -1226,12 +1265,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
});
});

Expand Down Expand Up @@ -1470,7 +1512,8 @@ export class DidDhtUtils {
bytesToPublicKey : Secp256k1.bytesToPublicKey,
privateKeyToBytes : Secp256k1.privateKeyToBytes,
bytesToPrivateKey : Secp256k1.bytesToPrivateKey,
}
},
X25519: X25519,
};

const converter = converters[curve];
Expand Down Expand Up @@ -1541,4 +1584,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
Expand Up @@ -52,6 +52,9 @@
}
]
},
"authoritativeGatewayUris": [
"gateway1.example-did-dht-gateway.com"
],
"dnsRecords": [
{
"name": "_did.cyuoqaf7itop8ohww4yn5ojg13qaq83r9zihgqntc5i9zwrfdfoo.",
Expand Down
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
Loading