Skip to content

Commit

Permalink
#497 - Added support for publishing NS records + official test vector…
Browse files Browse the repository at this point in the history
… 2 compliance (#514)

- Added support for publishing NS records and fixed bugs to achieve official test vector 2 compliance.
- Removed kid in DNS records according to DID DHT spec update.
- Minor renaming.
- QoL - Updated CODEOWNERS to further increase review efficiency.
- QoL - Added HTML code coverage output for `dids` repo for immediate coverage feedback.
  • Loading branch information
thehenrytsai authored May 7, 2024
1 parent b2e8831 commit 6c57350
Show file tree
Hide file tree
Showing 8 changed files with 275 additions and 96 deletions.
29 changes: 19 additions & 10 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,28 @@
# These owners will be the default owners for everything in the repo.
* @frankhinek @csuwildcat @mistermoe @thehenrytsai @lirancohen

# These are owners who can approve folders under the root directory and other CICD and QoL directories.
# Should be the union list of all owners of sub-directories, optionally minus the default owners.
/* @diehuxx @shamilovtim @nitro-neal
/.changeset @diehuxx @shamilovtim @nitro-neal
/.codesandbox @diehuxx @shamilovtim @nitro-neal
/.github @diehuxx @shamilovtim @nitro-neal
/.vscode @diehuxx @shamilovtim @nitro-neal
/scripts @diehuxx @shamilovtim @nitro-neal

# These are owners of any file in the `common`, `crypto`, `crypto-aws-kms`, `dids`, and
# `credentials` packages and their sub-directories.
/packages/common @diehuxx @mistermoe @frankhinek @thehenrytsai @nitro-neal
/packages/crypto @diehuxx @mistermoe @frankhinek @thehenrytsai
/packages/crypto-aws-kms @diehuxx @mistermoe @frankhinek @thehenrytsai
/packages/dids @diehuxx @mistermoe @frankhinek @thehenrytsai @nitro-neal
/packages/credentials @diehuxx @mistermoe @frankhinek @thehenrytsai @nitro-neal
/packages/common @diehuxx @thehenrytsai @nitro-neal
/packages/crypto @diehuxx @thehenrytsai
/packages/crypto-aws-kms @diehuxx @thehenrytsai
/packages/dids @diehuxx @thehenrytsai @nitro-neal
/packages/credentials @diehuxx @thehenrytsai @nitro-neal

# These are owners of any file in the `agent`, `user-agent`, `proxy-agent`, `identity-agent`, and
# `api` packages and their sub-directories.
/packages/agent @lirancohen @frankhinek @csuwildcat @mistermoe @shamilovtim
/packages/proxy-agent @lirancohen @frankhinek @csuwildcat @mistermoe @shamilovtim
/packages/user-agent @lirancohen @frankhinek @csuwildcat @mistermoe @shamilovtim
/packages/identity-agent @lirancohen @frankhinek @csuwildcat @mistermoe @shamilovtim
/packages/api @lirancohen @frankhinek @csuwildcat @mistermoe @shamilovtim @nitro-neal
/packages/agent @lirancohen @csuwildcat @shamilovtim
/packages/proxy-agent @lirancohen @csuwildcat @shamilovtim
/packages/user-agent @lirancohen @csuwildcat @shamilovtim
/packages/identity-agent @lirancohen @csuwildcat @shamilovtim
/packages/api @lirancohen @csuwildcat @shamilovtim @nitro-neal

1 change: 1 addition & 0 deletions packages/dids/.c8rc.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
],
"reporter": [
"cobertura",
"html",
"text"
]
}
94 changes: 65 additions & 29 deletions packages/dids/src/methods/did-dht.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Packet, TxtAnswer, TxtData } from '@dnsquery/dns-packet';
import type { Packet, StringAnswer, TxtAnswer, TxtData } from '@dnsquery/dns-packet';
import type {
Jwk,
Signer,
Expand Down Expand Up @@ -533,17 +533,17 @@ export class DidDht extends DidMethod {

// Generate random key material for the Identity Key and any additional verification methods.
// Add verification methods to the DID document.
for (const vm of verificationMethodsToAdd) {
for (const verificationMethod of verificationMethodsToAdd) {
// Generate a random key for the verification method, or if its the Identity Key's
// verification method (`id` is 0) use the key previously generated.
const keyUri = (vm.id && vm.id.split('#').pop() === '0')
const keyUri = (verificationMethod.id && verificationMethod.id.split('#').pop() === '0')
? identityKeyUri
: await keyManager.generateKey({ algorithm: vm.algorithm });
: await keyManager.generateKey({ algorithm: verificationMethod.algorithm });

const publicKey = await keyManager.getPublicKey({ keyUri });

// Use the given ID, the key's ID, or the key's thumbprint as the verification method ID.
let methodId = vm.id ?? publicKey.kid ?? await computeJwkThumbprint({ jwk: publicKey });
let methodId = verificationMethod.id ?? publicKey.kid ?? await computeJwkThumbprint({ jwk: publicKey });
methodId = `${didUri}#${extractDidFragment(methodId)}`; // Remove fragment prefix, if any.

// Initialize the `verificationMethod` array if it does not already exist.
Expand All @@ -553,12 +553,12 @@ export class DidDht extends DidMethod {
document.verificationMethod.push({
id : methodId,
type : 'JsonWebKey',
controller : vm.controller ?? didUri,
controller : verificationMethod.controller ?? didUri,
publicKeyJwk : publicKey,
});

// Add the verification method to the specified purpose properties of the DID document.
for (const purpose of vm.purposes ?? []) {
for (const purpose of verificationMethod.purposes ?? []) {
// Initialize the purpose property if it does not already exist.
if (!document[purpose]) document[purpose] = [];
// Add the verification method to the purpose property.
Expand Down Expand Up @@ -825,8 +825,9 @@ export class DidDhtDocument {
}): Promise<DidRegistrationResult> {
// Convert the DID document and DID metadata (such as DID types) to a DNS packet.
const dnsPacket = await DidDhtDocument.toDnsPacket({
didDocument : did.document,
didMetadata : did.metadata
didDocument : did.document,
didMetadata : did.metadata,
authoritativeGatewayUris : [gatewayUri]
});

// Create a signed BEP44 put message from the DNS packet.
Expand Down Expand Up @@ -1118,22 +1119,25 @@ export class DidDhtDocument {
* @param params - The parameters to use when converting a DID document to a DNS packet.
* @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.
* @returns A promise that resolves to a DNS packet.
*/
public static async toDnsPacket({ didDocument, didMetadata }: {
public static async toDnsPacket({ didDocument, didMetadata, authoritativeGatewayUris }: {
didDocument: DidDocument;
didMetadata: DidMetadata;
authoritativeGatewayUris?: string[];
}): Promise<Packet> {
const dnsAnswerRecords: TxtAnswer[] = [];
const txtRecords: TxtAnswer[] = [];
const nsRecords: StringAnswer[] = [];
const idLookup = new Map<string, string>();
const serviceIds: string[] = [];
const verificationMethodIds: string[] = [];

// Add DNS TXT records if the DID document contains an `alsoKnownAs` property.
if (didDocument.alsoKnownAs) {
dnsAnswerRecords.push({
txtRecords.push({
type : 'TXT',
name : '_aka.did.',
name : '_aka._did.',
ttl : DNS_RECORD_TTL,
data : didDocument.alsoKnownAs.join(VALUE_SEPARATOR)
});
Expand All @@ -1144,25 +1148,25 @@ export class DidDhtDocument {
const controller = Array.isArray(didDocument.controller)
? didDocument.controller.join(VALUE_SEPARATOR)
: didDocument.controller;
dnsAnswerRecords.push({
txtRecords.push({
type : 'TXT',
name : '_cnt.did.',
name : '_cnt._did.',
ttl : DNS_RECORD_TTL,
data : controller
});
}

// Add DNS TXT records for each verification method.
for (const [index, vm] of didDocument.verificationMethod?.entries() ?? []) {
for (const [index, verificationMethod] of didDocument.verificationMethod?.entries() ?? []) {
const dnsRecordId = `k${index}`;
verificationMethodIds.push(dnsRecordId);
let methodId = vm.id.split('#').pop()!; // Remove fragment prefix, if any.
let methodId = verificationMethod.id.split('#').pop()!; // Remove fragment prefix, if any.
idLookup.set(methodId, dnsRecordId);

const publicKey = vm.publicKeyJwk;
const publicKey = verificationMethod.publicKeyJwk;

if (!(publicKey?.crv && publicKey.crv in AlgorithmToKeyTypeMap)) {
throw new DidError(DidErrorCode.InvalidPublicKeyType, `Verification method '${vm.id}' contains an unsupported key type: ${publicKey?.crv ?? 'undefined'}`);
throw new DidError(DidErrorCode.InvalidPublicKeyType, `Verification method '${verificationMethod.id}' contains an unsupported key type: ${publicKey?.crv ?? 'undefined'}`);
}

// Use the public key's `crv` property to get the DID DHT key type.
Expand All @@ -1175,13 +1179,13 @@ export class DidDhtDocument {
const publicKeyBase64Url = Convert.uint8Array(publicKeyBytes).toBase64Url();

// Define the data for the DNS TXT record.
const txtData = [`id=${methodId}`, `t=${keyType}`, `k=${publicKeyBase64Url}`];
const txtData = [`t=${keyType}`, `k=${publicKeyBase64Url}`];

// Add the controller property, if set to a value other than the Identity Key (DID Subject).
if (vm.controller !== didDocument.id) txtData.push(`c=${vm.controller}`);
if (verificationMethod.controller !== didDocument.id) txtData.push(`c=${verificationMethod.controller}`);

// Add a TXT record for the verification method.
dnsAnswerRecords.push({
txtRecords.push({
type : 'TXT',
name : `_${dnsRecordId}._did.`,
ttl : DNS_RECORD_TTL,
Expand All @@ -1203,7 +1207,7 @@ export class DidDhtDocument {
);

// Add a TXT record for the verification method.
dnsAnswerRecords.push({
txtRecords.push({
type : 'TXT',
name : `_${dnsRecordId}._did.`,
ttl : DNS_RECORD_TTL,
Expand Down Expand Up @@ -1244,7 +1248,7 @@ export class DidDhtDocument {
const types = didMetadata.types as (DidDhtRegisteredDidType | keyof typeof DidDhtRegisteredDidType)[];
const typeIntegers = types.map(type => typeof type === 'string' ? DidDhtRegisteredDidType[type] : type);

dnsAnswerRecords.push({
txtRecords.push({
type : 'TXT',
name : '_typ._did.',
ttl : DNS_RECORD_TTL,
Expand All @@ -1253,19 +1257,29 @@ export class DidDhtDocument {
}

// Add a DNS TXT record for the root record.
dnsAnswerRecords.push({
txtRecords.push({
type : 'TXT',
name : '_did.' + DidDhtDocument.getUniqueDidSuffix(didDocument.id) + '.', // name of a Root Record MUST end in `<ID>.`
ttl : DNS_RECORD_TTL,
data : rootRecord.join(PROPERTY_SEPARATOR)
});

// Add an NS record for each authoritative gateway URI.
for (const gatewayUri of authoritativeGatewayUris || []) {
nsRecords.push({
type : 'NS',
name : '_did.' + DidDhtDocument.getUniqueDidSuffix(didDocument.id) + '.', // name of an NS record a authoritative gateway MUST end in `<ID>.`
ttl : DNS_RECORD_TTL,
data : gatewayUri + '.'
});
}

// Create a DNS response packet with the authoritative answer flag set.
const dnsPacket: Packet = {
id : 0,
type : 'response',
flags : AUTHORITATIVE_ANSWER,
answers : dnsAnswerRecords
answers : [...txtRecords, ...nsRecords]
};

return dnsPacket;
Expand Down Expand Up @@ -1412,9 +1426,31 @@ export class DidDhtUtils {
*/
public static keyConverter(curve: string): AsymmetricKeyConverter {
const converters: Record<string, AsymmetricKeyConverter> = {
'Ed25519' : Ed25519,
'P-256' : Secp256r1,
'secp256k1' : Secp256k1
'Ed25519' : Ed25519,
'P-256' : {
// Wrap the key converter which produces uncompressed public key bytes to produce compressed key bytes as required by the DID DHT spec.
// See https://did-dht.com/#representing-keys for more info.
publicKeyToBytes: async ({ publicKey }: { publicKey: Jwk }): Promise<Uint8Array> => {
const publicKeyBytes = await Secp256r1.publicKeyToBytes({ publicKey });
const compressedPublicKey = await Secp256r1.compressPublicKey({ publicKeyBytes });
return compressedPublicKey;
},
bytesToPublicKey : Secp256r1.bytesToPublicKey,
privateKeyToBytes : Secp256r1.privateKeyToBytes,
bytesToPrivateKey : Secp256r1.bytesToPrivateKey,
},
'secp256k1': {
// Wrap the key converter which produces uncompressed public key bytes to produce compressed key bytes as required by the DID DHT spec.
// See https://did-dht.com/#representing-keys for more info.
publicKeyToBytes: async ({ publicKey }: { publicKey: Jwk }): Promise<Uint8Array> => {
const publicKeyBytes = await Secp256k1.publicKeyToBytes({ publicKey });
const compressedPublicKey = await Secp256k1.compressPublicKey({ publicKeyBytes });
return compressedPublicKey;
},
bytesToPublicKey : Secp256k1.bytesToPublicKey,
privateKeyToBytes : Secp256k1.privateKeyToBytes,
bytesToPrivateKey : Secp256k1.bytesToPrivateKey,
}
};

const converter = converters[curve];
Expand Down

This file was deleted.

This file was deleted.

45 changes: 45 additions & 0 deletions packages/dids/tests/fixtures/test-vectors/did-dht/vector-1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"didDocument": {
"id": "did:dht:cyuoqaf7itop8ohww4yn5ojg13qaq83r9zihgqntc5i9zwrfdfoo",
"verificationMethod": [
{
"id": "did:dht:cyuoqaf7itop8ohww4yn5ojg13qaq83r9zihgqntc5i9zwrfdfoo#0",
"type": "JsonWebKey",
"controller": "did:dht:cyuoqaf7itop8ohww4yn5ojg13qaq83r9zihgqntc5i9zwrfdfoo",
"publicKeyJwk": {
"kid": "0",
"alg": "Ed25519",
"crv": "Ed25519",
"kty": "OKP",
"x": "YCcHYL2sYNPDlKaALcEmll2HHyT968M4UWbr-9CFGWE"
}
}
],
"authentication": [
"did:dht:cyuoqaf7itop8ohww4yn5ojg13qaq83r9zihgqntc5i9zwrfdfoo#0"
],
"assertionMethod": [
"did:dht:cyuoqaf7itop8ohww4yn5ojg13qaq83r9zihgqntc5i9zwrfdfoo#0"
],
"capabilityInvocation": [
"did:dht:cyuoqaf7itop8ohww4yn5ojg13qaq83r9zihgqntc5i9zwrfdfoo#0"
],
"capabilityDelegation": [
"did:dht:cyuoqaf7itop8ohww4yn5ojg13qaq83r9zihgqntc5i9zwrfdfoo#0"
]
},
"dnsRecords": [
{
"name": "_did.cyuoqaf7itop8ohww4yn5ojg13qaq83r9zihgqntc5i9zwrfdfoo.",
"type": "TXT",
"ttl": 7200,
"rdata": "v=0;vm=k0;auth=k0;asm=k0;inv=k0;del=k0"
},
{
"name": "_k0._did.",
"type": "TXT",
"ttl": 7200,
"rdata": "t=0;k=YCcHYL2sYNPDlKaALcEmll2HHyT968M4UWbr-9CFGWE"
}
]
}
Loading

0 comments on commit 6c57350

Please sign in to comment.