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

Added support for publishing NS records + official test vector 2 compliance #514

Merged
merged 6 commits into from
May 7, 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
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",
nitro-neal marked this conversation as resolved.
Show resolved Hide resolved
"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}`];

nitro-neal marked this conversation as resolved.
Show resolved Hide resolved
// 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 + '.'
nitro-neal marked this conversation as resolved.
Show resolved Hide resolved
});
}

// 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
Loading