@enbox/dids
Version:
TBD DIDs library
888 lines • 68.5 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
t[p[i]] = s[p[i]];
}
return t;
};
import bencode from 'bencode';
import { Convert } from '@enbox/common';
import { computeJwkThumbprint, Ed25519, LocalKeyManager, Secp256k1, Secp256r1, X25519 } from '@enbox/crypto';
import { AUTHORITATIVE_ANSWER, decode as dnsPacketDecode, encode as dnsPacketEncode } from '@dnsquery/dns-packet';
import { Did } from '../did.js';
import { DidMethod } from './did-method.js';
import { BearerDid } from '../bearer-did.js';
import { extractDidFragment } from '../utils.js';
import { DidError, DidErrorCode } from '../did-error.js';
import { DidVerificationRelationship } from '../types/did-core.js';
import { EMPTY_DID_RESOLUTION_RESULT } from '../types/did-resolution.js';
/**
* The default DID DHT Gateway or Pkarr Relay server to use when publishing and resolving DID
* documents.
*/
const DEFAULT_GATEWAY_URI = 'https://did-dht-production.up.railway.app';
/**
* The version of the DID DHT specification that is implemented by this library.
*
* When a DID DHT document is published to the DHT network, the version of the specification that
* was used to create the document is included in the DNS TXT record for the root record. This
* allows clients to determine whether the DID DHT document is compatible with the client's
* implementation of the DID DHT specification. The version number is not present in the
* corresponding DID document.
*
* @see {@link https://did-dht.com | DID DHT Method Specification}
*/
const DID_DHT_SPECIFICATION_VERSION = 0;
/**
* The default TTL for DNS records published to the DHT network.
*
* The recommended TTL value is 7200 seconds (2 hours) since it matches the default TTL for
* Mainline DHT records.
*/
const DNS_RECORD_TTL = 7200;
/**
* Character used to separate distinct elements or entries in the DNS packet representation
* of a DID Document.
*
* For example, verification methods, verification relationships, and services are separated by
* semicolons (`;`) in the root record:
* ```
* vm=k1;auth=k1;asm=k2;inv=k3;del=k3;srv=s1
* ```
*/
const PROPERTY_SEPARATOR = ';';
/**
* Character used to separate distinct values within a single element or entry in the DNS packet
* representation of a DID Document.
*
* For example, multiple key references for the `authentication` verification relationships are
* separated by commas (`,`):
* ```
* auth=0,1,2
* ```
*/
const VALUE_SEPARATOR = ',';
/**
* Represents an optional extension to a DID Document’s DNS packet representation exposed as a
* type index.
*
* Type indexing is an OPTIONAL feature that enables DIDs to become discoverable. DIDs that wish to
* be discoverable and resolveable by type can include one or more types when publishing their DID
* document to a DID DHT Gateway.
*
* The registered DID types are published in the {@link https://did-dht.com/registry/index.html#indexed-types | DID DHT Registry}.
*/
export var DidDhtRegisteredDidType;
(function (DidDhtRegisteredDidType) {
/**
* Type 0 is reserved for DIDs that do not wish to associate themselves with a specific type but
* wish to make themselves discoverable.
*/
DidDhtRegisteredDidType[DidDhtRegisteredDidType["Discoverable"] = 0] = "Discoverable";
/**
* Organization
* @see {@link https://schema.org/Organization | schema definition}
*/
DidDhtRegisteredDidType[DidDhtRegisteredDidType["Organization"] = 1] = "Organization";
/**
* Government Organization
* @see {@link https://schema.org/GovernmentOrganization | schema definition}
*/
DidDhtRegisteredDidType[DidDhtRegisteredDidType["Government"] = 2] = "Government";
/**
* Corporation
* @see {@link https://schema.org/Corporation | schema definition}
*/
DidDhtRegisteredDidType[DidDhtRegisteredDidType["Corporation"] = 3] = "Corporation";
/**
* Corporation
* @see {@link https://schema.org/Corporation | schema definition}
*/
DidDhtRegisteredDidType[DidDhtRegisteredDidType["LocalBusiness"] = 4] = "LocalBusiness";
/**
* Software Package
* @see {@link https://schema.org/SoftwareSourceCode | schema definition}
*/
DidDhtRegisteredDidType[DidDhtRegisteredDidType["SoftwarePackage"] = 5] = "SoftwarePackage";
/**
* Web App
* @see {@link https://schema.org/WebApplication | schema definition}
*/
DidDhtRegisteredDidType[DidDhtRegisteredDidType["WebApp"] = 6] = "WebApp";
/**
* Financial Institution
* @see {@link https://schema.org/FinancialService | schema definition}
*/
DidDhtRegisteredDidType[DidDhtRegisteredDidType["FinancialInstitution"] = 7] = "FinancialInstitution";
})(DidDhtRegisteredDidType || (DidDhtRegisteredDidType = {}));
/**
* Enumerates the types of keys that can be used in a DID DHT document.
*
* The DID DHT method supports various cryptographic key types. These key types are essential for
* the creation and management of DIDs and their associated cryptographic operations like signing
* and encryption. The registered key types are published in the DID DHT Registry and each is
* assigned a unique numerical value for use by client and gateway implementations.
*
* The registered key types are published in the {@link https://did-dht.com/registry/index.html#key-type-index | DID DHT Registry}.
*/
export var DidDhtRegisteredKeyType;
(function (DidDhtRegisteredKeyType) {
/**
* Ed25519: A public-key signature system using the EdDSA (Edwards-curve Digital Signature
* Algorithm) and Curve25519.
*/
DidDhtRegisteredKeyType[DidDhtRegisteredKeyType["Ed25519"] = 0] = "Ed25519";
/**
* secp256k1: A cryptographic curve used for digital signatures in a range of decentralized
* systems.
*/
DidDhtRegisteredKeyType[DidDhtRegisteredKeyType["secp256k1"] = 1] = "secp256k1";
/**
* 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.
*/
DidDhtRegisteredKeyType[DidDhtRegisteredKeyType["secp256r1"] = 2] = "secp256r1";
/**
* X25519: A public key used for Diffie-Hellman key exchange using Curve25519.
*/
DidDhtRegisteredKeyType[DidDhtRegisteredKeyType["X25519"] = 3] = "X25519";
})(DidDhtRegisteredKeyType || (DidDhtRegisteredKeyType = {}));
/**
* Maps {@link https://www.w3.org/TR/did-core/#verification-relationships | DID Core Verification Relationship}
* values to the corresponding record name in the DNS packet representation of a DHT DID document.
*/
export var DidDhtVerificationRelationship;
(function (DidDhtVerificationRelationship) {
/**
* Specifies how the DID subject is expected to be authenticated.
*/
DidDhtVerificationRelationship["authentication"] = "auth";
/**
* Specifies how the DID subject is expected to express claims, such as for issuing Verifiable
* Credentials.
*/
DidDhtVerificationRelationship["assertionMethod"] = "asm";
/**
* Specifies a mechanism used by the DID subject to delegate a cryptographic capability to another
* party
*/
DidDhtVerificationRelationship["capabilityDelegation"] = "del";
/**
* Specifies a verification method used by the DID subject to invoke a cryptographic capability.
*/
DidDhtVerificationRelationship["capabilityInvocation"] = "inv";
/**
* Specifies how an entity can generate encryption material to communicate confidentially with the
* DID subject.
*/
DidDhtVerificationRelationship["keyAgreement"] = "agm";
})(DidDhtVerificationRelationship || (DidDhtVerificationRelationship = {}));
/**
* Private helper that maps algorithm identifiers to their corresponding DID DHT
* {@link DidDhtRegisteredKeyType | registered key type}.
*/
const AlgorithmToKeyTypeMap = {
Ed25519: DidDhtRegisteredKeyType.Ed25519,
ES256K: DidDhtRegisteredKeyType.secp256k1,
ES256: DidDhtRegisteredKeyType.secp256r1,
'P-256': DidDhtRegisteredKeyType.secp256r1,
secp256k1: DidDhtRegisteredKeyType.secp256k1,
secp256r1: DidDhtRegisteredKeyType.secp256r1,
X25519: DidDhtRegisteredKeyType.X25519,
};
/**
* Private helper that maps did dht registered key types to their corresponding default algorithm identifiers.
*/
const KeyTypeToDefaultAlgorithmMap = {
[DidDhtRegisteredKeyType.Ed25519]: 'EdDSA',
[DidDhtRegisteredKeyType.secp256k1]: 'ES256K',
[DidDhtRegisteredKeyType.secp256r1]: 'ES256',
[DidDhtRegisteredKeyType.X25519]: 'ECDH-ES+A256KW',
};
/**
* The `DidDht` class provides an implementation of the `did:dht` DID method.
*
* Features:
* - DID Creation: Create new `did:dht` DIDs.
* - DID Key Management: Instantiate a DID object from an existing verification method keys or
* or a key in a Key Management System (KMS). If supported by the KMS, a DID's
* key can be exported to a portable DID format.
* - DID Resolution: Resolve a `did:dht` to its corresponding DID Document stored in the DHT network.
* - Signature Operations: Sign and verify messages using keys associated with a DID.
*
* @remarks
* The `did:dht` method leverages the distributed nature of the Mainline DHT network for
* decentralized identity management. This method allows DIDs to be resolved without relying on
* centralized registries or ledgers, enhancing privacy and control for users. The DID Document is
* stored and retrieved from the DHT network, and the method includes optional mechanisms for
* discovering DIDs by type.
*
* The DID URI in the `did:dht` method includes a method-specific identifier called the Identity Key
* which corresponds to the DID's entry in the DHT network. The Identity Key required to make
* changes to the DID Document since Mainline DHT nodes validate the signature of each message
* before storing the value in the DHT.
*
* @see {@link https://did-dht.com | DID DHT Method Specification}
*
* @example
* ```ts
* // DID Creation
* const did = await DidDht.create();
*
* // DID Creation with a KMS
* const keyManager = new LocalKeyManager();
* const did = await DidDht.create({ keyManager });
*
* // DID Resolution
* const resolutionResult = await DidDht.resolve({ did: did.uri });
*
* // Signature Operations
* const signer = await did.getSigner();
* const signature = await signer.sign({ data: new TextEncoder().encode('Message') });
* const isValid = await signer.verify({ data: new TextEncoder().encode('Message'), signature });
*
* // Import / Export
*
* // Export a BearerDid object to the PortableDid format.
* const portableDid = await did.export();
*
* // Reconstruct a BearerDid object from a PortableDid
* const did = await DidDht.import(portableDid);
* ```
*/
export class DidDht extends DidMethod {
/**
* Creates a new DID using the `did:dht` method formed from a newly generated key.
*
* @remarks
* The DID URI is formed by z-base-32 encoding the Identity Key public key and prefixing with
* `did:dht:`.
*
* Notes:
* - If no `options` are given, by default a new Ed25519 key will be generated which serves as the
* Identity Key.
*
* @example
* ```ts
* // DID Creation
* const did = await DidDht.create();
*
* // DID Creation with a KMS
* const keyManager = new LocalKeyManager();
* const did = await DidDht.create({ keyManager });
* ```
*
* @param params - The parameters for the create operation.
* @param params.keyManager - Optionally specify a Key Management System (KMS) used to generate
* keys and sign data.
* @param params.options - Optional parameters that can be specified when creating a new DID.
* @returns A Promise resolving to a {@link BearerDid} object representing the new DID.
*/
static create() {
return __awaiter(this, arguments, void 0, function* ({ keyManager = new LocalKeyManager(), options = {} } = {}) {
// Before processing the create operation, validate DID-method-specific requirements to prevent
// keys from being generated unnecessarily.
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
// Check 1: Validate that the algorithm for any given verification method is supported by the
// DID DHT specification.
if ((_a = options.verificationMethods) === null || _a === void 0 ? void 0 : _a.some(vm => !(vm.algorithm in AlgorithmToKeyTypeMap))) {
throw new Error('One or more verification method algorithms are not supported');
}
// Check 2: Validate that the ID for any given verification method is unique.
const methodIds = (_b = options.verificationMethods) === null || _b === void 0 ? void 0 : _b.filter(vm => 'id' in vm).map(vm => vm.id);
if (methodIds && methodIds.length !== new Set(methodIds).size) {
throw new Error('One or more verification method IDs are not unique');
}
// Check 3: Validate that the required properties for any given services are present.
if ((_c = options.services) === null || _c === void 0 ? void 0 : _c.some(s => !s.id || !s.type || !s.serviceEndpoint)) {
throw new Error('One or more services are missing required properties');
}
// Generate random key material for the Identity Key.
const identityKeyUri = yield keyManager.generateKey({ algorithm: 'Ed25519' });
const identityKey = yield keyManager.getPublicKey({ keyUri: identityKeyUri });
// Compute the DID URI from the Identity Key.
const didUri = yield DidDhtUtils.identityKeyToIdentifier({ identityKey });
// Begin constructing the DID Document.
const document = Object.assign(Object.assign({ id: didUri }, options.alsoKnownAs && { alsoKnownAs: options.alsoKnownAs }), options.controllers && { controller: options.controllers });
// If the given verification methods do not contain an Identity Key, add one.
const verificationMethodsToAdd = [...(_d = options.verificationMethods) !== null && _d !== void 0 ? _d : []];
if (!(verificationMethodsToAdd === null || verificationMethodsToAdd === void 0 ? void 0 : verificationMethodsToAdd.some(vm => { var _a; return ((_a = vm.id) === null || _a === void 0 ? void 0 : _a.split('#').pop()) === '0'; }))) {
// Add the Identity Key to the beginning of the key set.
verificationMethodsToAdd.unshift({
algorithm: 'Ed25519',
id: '0',
purposes: ['authentication', 'assertionMethod', 'capabilityDelegation', 'capabilityInvocation']
});
}
// Generate random key material for the Identity Key and any additional verification methods.
// Add verification methods to the DID document.
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 = (verificationMethod.id && verificationMethod.id.split('#').pop() === '0')
? identityKeyUri
: yield keyManager.generateKey({ algorithm: verificationMethod.algorithm });
const publicKey = yield keyManager.getPublicKey({ keyUri });
// Use the given ID, the key's ID, or the key's thumbprint as the verification method ID.
let methodId = (_f = (_e = verificationMethod.id) !== null && _e !== void 0 ? _e : publicKey.kid) !== null && _f !== void 0 ? _f : yield computeJwkThumbprint({ jwk: publicKey });
methodId = `${didUri}#${extractDidFragment(methodId)}`; // Remove fragment prefix, if any.
// Initialize the `verificationMethod` array if it does not already exist.
(_g = document.verificationMethod) !== null && _g !== void 0 ? _g : (document.verificationMethod = []);
// Add the verification method to the DID document.
document.verificationMethod.push({
id: methodId,
type: 'JsonWebKey',
controller: (_h = verificationMethod.controller) !== null && _h !== void 0 ? _h : didUri,
publicKeyJwk: publicKey,
});
// Add the verification method to the specified purpose properties of the DID document.
for (const purpose of (_j = verificationMethod.purposes) !== null && _j !== void 0 ? _j : []) {
// Initialize the purpose property if it does not already exist.
if (!document[purpose])
document[purpose] = [];
// Add the verification method to the purpose property.
document[purpose].push(methodId);
}
}
// Add services, if any, to the DID document.
(_k = options.services) === null || _k === void 0 ? void 0 : _k.forEach(service => {
var _a;
(_a = document.service) !== null && _a !== void 0 ? _a : (document.service = []);
service.id = `${didUri}#${service.id.split('#').pop()}`; // Remove fragment prefix, if any.
document.service.push(service);
});
// Create the BearerDid object, including the registered DID types (if any), and specify that
// the DID has not yet been published.
const did = new BearerDid({
uri: didUri,
document,
metadata: Object.assign({ published: false }, options.types && { types: options.types }),
keyManager
});
// By default, publish the DID document to a DHT Gateway unless explicitly disabled.
if ((_l = options.publish) !== null && _l !== void 0 ? _l : true) {
const registrationResult = yield DidDht.publish({ did, gatewayUri: options.gatewayUri });
did.metadata = registrationResult.didDocumentMetadata;
}
return did;
});
}
/**
* Instantiates a {@link BearerDid} object for the DID DHT method from a given {@link PortableDid}.
*
* This method allows for the creation of a `BearerDid` object using a previously created DID's
* key material, DID document, and metadata.
*
* @example
* ```ts
* // Export an existing BearerDid to PortableDid format.
* const portableDid = await did.export();
* // Reconstruct a BearerDid object from the PortableDid.
* const did = await DidDht.import({ portableDid });
* ```
*
* @param params - The parameters for the import operation.
* @param params.portableDid - The PortableDid object to import.
* @param params.keyManager - Optionally specify an external Key Management System (KMS) used to
* generate keys and sign data. If not given, a new
* {@link LocalKeyManager} instance will be created and
* used.
* @returns A Promise resolving to a `BearerDid` object representing the DID formed from the
* provided PortableDid.
* @throws An error if the PortableDid document does not contain any verification methods, lacks
* an Identity Key, or the keys for any verification method are missing in the key
* manager.
*/
static import(_a) {
return __awaiter(this, arguments, void 0, function* ({ portableDid, keyManager = new LocalKeyManager() }) {
var _b;
// Verify the DID method is supported.
const parsedDid = Did.parse(portableDid.uri);
if ((parsedDid === null || parsedDid === void 0 ? void 0 : parsedDid.method) !== DidDht.methodName) {
throw new DidError(DidErrorCode.MethodNotSupported, `Method not supported`);
}
const did = yield BearerDid.import({ portableDid, keyManager });
// Validate that the given verification methods contain an Identity Key.
if (!((_b = did.document.verificationMethod) === null || _b === void 0 ? void 0 : _b.some(vm => { var _a; return ((_a = vm.id) === null || _a === void 0 ? void 0 : _a.split('#').pop()) === '0'; }))) {
throw new DidError(DidErrorCode.InvalidDidDocument, `DID document must contain an Identity Key`);
}
return did;
});
}
/**
* Given the W3C DID Document of a `did:dht` DID, return the verification method that will be used
* for signing messages and credentials. If given, the `methodId` parameter is used to select the
* verification method. If not given, the Identity Key's verification method with an ID fragment
* of '#0' is used.
*
* @param params - The parameters for the `getSigningMethod` operation.
* @param params.didDocument - DID Document to get the verification method from.
* @param params.methodId - ID of the verification method to use for signing.
* @returns Verification method to use for signing.
*/
static getSigningMethod(_a) {
return __awaiter(this, arguments, void 0, function* ({ didDocument, methodId = '#0' }) {
var _b;
// Verify the DID method is supported.
const parsedDid = Did.parse(didDocument.id);
if (parsedDid && parsedDid.method !== this.methodName) {
throw new DidError(DidErrorCode.MethodNotSupported, `Method not supported: ${parsedDid.method}`);
}
// Attempt to find a verification method that matches the given method ID, or if not given,
// find the first verification method intended for signing claims.
const verificationMethod = (_b = didDocument.verificationMethod) === null || _b === void 0 ? void 0 : _b.find(vm => { var _a, _b; return extractDidFragment(vm.id) === ((_a = extractDidFragment(methodId)) !== null && _a !== void 0 ? _a : extractDidFragment((_b = didDocument.assertionMethod) === null || _b === void 0 ? void 0 : _b[0])); });
if (!(verificationMethod && verificationMethod.publicKeyJwk)) {
throw new DidError(DidErrorCode.InternalError, 'A verification method intended for signing could not be determined from the DID Document');
}
return verificationMethod;
});
}
/**
* Publishes a DID to the DHT, making it publicly discoverable and resolvable.
*
* This method handles the publication of a DID Document associated with a `did:dht` DID to the
* Mainline DHT network. The publication process involves storing the DID Document in Mainline DHT
* via a Pkarr relay server.
*
* @remarks
* - This method is typically invoked automatically during the creation of a new DID unless the
* `publish` option is set to `false`.
* - For existing, unpublished DIDs, it can be used to publish the DID Document to Mainline DHT.
* - The method relies on the specified Pkarr relay server to interface with the DHT network.
*
* @example
* ```ts
* // Generate a new DID and keys but explicitly disable publishing.
* const did = await DidDht.create({ options: { publish: false } });
* // Publish the DID to the DHT.
* const registrationResult = await DidDht.publish({ did });
* // `registrationResult.didDocumentMetadata.published` is true if the DID was successfully published.
* ```
*
* @param params - The parameters for the `publish` operation.
* @param params.did - The `BearerDid` object representing the DID to be published.
* @param params.gatewayUri - Optional. The URI of a server involved in executing DID method
* operations. In the context of publishing, the endpoint is expected
* to be a DID DHT Gateway or Pkarr Relay. If not specified, a default
* gateway node is used.
* @returns A promise that resolves to a {@link DidRegistrationResult} object that contains
* the result of registering the DID with a DID DHT Gateway or Pkarr relay.
*/
static publish(_a) {
return __awaiter(this, arguments, void 0, function* ({ did, gatewayUri = DEFAULT_GATEWAY_URI }) {
const registrationResult = yield DidDhtDocument.put({ did, gatewayUri });
return registrationResult;
});
}
/**
* Resolves a `did:dht` identifier to its corresponding DID document.
*
* This method performs the resolution of a `did:dht` DID, retrieving its DID Document from the
* Mainline DHT network. The process involves querying the DHT network via a Pkarr relay server to
* retrieve the DID Document that corresponds to the given DID identifier.
*
* @remarks
* - If a `gatewayUri` option is not specified, a default Pkarr relay is used to access the DHT
* network.
* - It decodes the DID identifier and retrieves the associated DID Document and metadata.
* - In case of resolution failure, appropriate error information is returned.
*
* @example
* ```ts
* const resolutionResult = await DidDht.resolve('did:dht:example');
* ```
*
* @param didUri - The DID to be resolved.
* @param options - Optional parameters for resolving the DID. Unused by this DID method.
* @returns A Promise resolving to a {@link DidResolutionResult} object representing the result of
* the resolution.
*/
static resolve(didUri_1) {
return __awaiter(this, arguments, void 0, function* (didUri, options = {}) {
var _a;
// To execute the read method operation, use the given gateway URI or a default.
const gatewayUri = (_a = options === null || options === void 0 ? void 0 : options.gatewayUri) !== null && _a !== void 0 ? _a : DEFAULT_GATEWAY_URI;
try {
// Attempt to decode the z-base-32-encoded identifier.
yield DidDhtUtils.identifierToIdentityKey({ didUri });
// Attempt to retrieve the DID document and metadata from the DHT network.
const { didDocument, didDocumentMetadata } = yield DidDhtDocument.get({ didUri, gatewayUri });
// If the DID document was retrieved successfully, return it.
return Object.assign(Object.assign({}, EMPTY_DID_RESOLUTION_RESULT), { didDocument,
didDocumentMetadata });
}
catch (error) {
// Rethrow any unexpected errors that are not a `DidError`.
if (!(error instanceof DidError))
throw new Error(error);
// Return a DID Resolution Result with the appropriate error code.
return Object.assign(Object.assign({}, EMPTY_DID_RESOLUTION_RESULT), { didResolutionMetadata: Object.assign({ error: error.code }, error.message && { errorMessage: error.message }) });
}
});
}
}
/**
* Name of the DID method, as defined in the DID DHT specification.
*/
DidDht.methodName = 'dht';
/**
* The `DidDhtDocument` class provides functionality for interacting with the DID document stored in
* Mainline DHT in support of DID DHT method create, resolve, update, and deactivate operations.
*
* This class includes methods for retrieving and publishing DID documents to and from the DHT,
* using DNS packet encoding and DID DHT Gateway or Pkarr Relay servers.
*/
export class DidDhtDocument {
/**
* Retrieves a DID document and its metadata from the DHT network.
*
* @param params - The parameters for the get operation.
* @param params.didUri - The DID URI containing the Identity Key.
* @param params.gatewayUri - The DID DHT Gateway or Pkarr Relay URI.
* @returns A Promise resolving to a {@link DidResolutionResult} object containing the DID
* document and its metadata.
*/
static get(_a) {
return __awaiter(this, arguments, void 0, function* ({ didUri, gatewayUri }) {
// Decode the z-base-32 DID identifier to public key as a byte array.
const publicKeyBytes = DidDhtUtils.identifierToIdentityKeyBytes({ didUri });
// Retrieve the signed BEP44 message from a DID DHT Gateway or Pkarr relay.
const bep44Message = yield DidDhtDocument.pkarrGet({ gatewayUri, publicKeyBytes });
// Verify the signature of the BEP44 message and parse the value to a DNS packet.
const dnsPacket = yield DidDhtUtils.parseBep44GetMessage({ bep44Message });
// Convert the DNS packet to a DID document and metadata.
const resolutionResult = yield DidDhtDocument.fromDnsPacket({ didUri, dnsPacket });
// Set the version ID of the DID document metadata to the sequence number of the BEP44 message.
resolutionResult.didDocumentMetadata.versionId = bep44Message.seq.toString();
return resolutionResult;
});
}
/**
* Publishes a DID document to the DHT network.
*
* @param params - The parameters to use when publishing the DID document to the DHT network.
* @param params.did - The DID object whose DID document will be published.
* @param params.gatewayUri - The DID DHT Gateway or Pkarr Relay URI.
* @returns A promise that resolves to a {@link DidRegistrationResult} object that contains
* the result of registering the DID with a DID DHT Gateway or Pkarr relay.
*/
static put(_a) {
return __awaiter(this, arguments, void 0, function* ({ did, gatewayUri }) {
// Convert the DID document and DID metadata (such as DID types) to a DNS packet.
const dnsPacket = yield DidDhtDocument.toDnsPacket({
didDocument: did.document,
didMetadata: did.metadata,
authoritativeGatewayUris: [gatewayUri]
});
// Create a signed BEP44 put message from the DNS packet.
const bep44Message = yield DidDhtUtils.createBep44PutMessage({
dnsPacket,
publicKeyBytes: DidDhtUtils.identifierToIdentityKeyBytes({ didUri: did.uri }),
signer: yield did.getSigner({ methodId: '0' })
});
// Publish the DNS packet to the DHT network.
const putResult = yield DidDhtDocument.pkarrPut({ gatewayUri, bep44Message });
// Return the result of processing the PUT operation, including the updated DID metadata with
// the version ID and the publishing result.
return {
didDocument: did.document,
didDocumentMetadata: Object.assign(Object.assign({}, did.metadata), { published: putResult, versionId: bep44Message.seq.toString() }),
didRegistrationMetadata: {}
};
});
}
/**
* Retrieves a signed BEP44 message from a DID DHT Gateway or Pkarr Relay server.
*
* @see {@link https://github.com/Nuhvi/pkarr/blob/main/design/relays.md | Pkarr Relay design}
*
* @param params
* @param params.gatewayUri - The DID DHT Gateway or Pkarr Relay URI.
* @param params.publicKeyBytes - The public key bytes of the Identity Key, z-base-32 encoded.
* @returns A promise resolving to a BEP44 message containing the signed DNS packet.
*/
static pkarrGet(_a) {
return __awaiter(this, arguments, void 0, function* ({ gatewayUri, publicKeyBytes }) {
// The identifier (key in the DHT) is the z-base-32 encoding of the Identity Key.
const identifier = Convert.uint8Array(publicKeyBytes).toBase32Z();
// Concatenate the gateway URI with the identifier to form the full URL.
const url = new URL(identifier, gatewayUri).href;
// Transmit the Get request to the DID DHT Gateway or Pkarr Relay and get the response.
let response;
try {
response = yield fetch(url, { method: 'GET' });
if (!response.ok) {
throw new DidError(DidErrorCode.NotFound, `Pkarr record not found for: ${identifier}`);
}
}
catch (error) {
if (error instanceof DidError)
throw error;
throw new DidError(DidErrorCode.InternalError, `Failed to fetch Pkarr record: ${error.message}`);
}
// Read the Fetch Response stream into a byte array.
const messageBytes = yield response.arrayBuffer();
if (!messageBytes) {
throw new DidError(DidErrorCode.NotFound, `Pkarr record not found for: ${identifier}`);
}
if (messageBytes.byteLength < 72) {
throw new DidError(DidErrorCode.InvalidDidDocumentLength, `Pkarr response must be at least 72 bytes but got: ${messageBytes.byteLength}`);
}
if (messageBytes.byteLength > 1072) {
throw new DidError(DidErrorCode.InvalidDidDocumentLength, `Pkarr response exceeds 1000 byte limit: ${messageBytes.byteLength}`);
}
// Decode the BEP44 message from the byte array.
const bep44Message = {
k: publicKeyBytes,
seq: Number(new DataView(messageBytes).getBigUint64(64)),
sig: new Uint8Array(messageBytes, 0, 64),
v: new Uint8Array(messageBytes, 72)
};
return bep44Message;
});
}
/**
* Publishes a signed BEP44 message to a DID DHT Gateway or Pkarr Relay server.
*
* @see {@link https://github.com/Nuhvi/pkarr/blob/main/design/relays.md | Pkarr Relay design}
*
* @param params - The parameters to use when publishing a signed BEP44 message to a Pkarr relay server.
* @param params.gatewayUri - The DID DHT Gateway or Pkarr Relay URI.
* @param params.bep44Message - The BEP44 message to be published, containing the signed DNS packet.
* @returns A promise resolving to `true` if the message was successfully published, otherwise `false`.
*/
static pkarrPut(_a) {
return __awaiter(this, arguments, void 0, function* ({ gatewayUri, bep44Message }) {
// The identifier (key in the DHT) is the z-base-32 encoding of the Identity Key.
const identifier = Convert.uint8Array(bep44Message.k).toBase32Z();
// Concatenate the gateway URI with the identifier to form the full URL.
const url = new URL(identifier, gatewayUri).href;
// Construct the body of the request according to the Pkarr relay specification.
const body = new Uint8Array(bep44Message.v.length + 72);
body.set(bep44Message.sig, 0);
new DataView(body.buffer).setBigUint64(bep44Message.sig.length, BigInt(bep44Message.seq));
body.set(bep44Message.v, bep44Message.sig.length + 8);
// Transmit the Put request to the Pkarr relay and get the response.
let response;
try {
response = yield fetch(url, {
method: 'PUT',
headers: { 'Content-Type': 'application/octet-stream' },
body
});
}
catch (error) {
throw new DidError(DidErrorCode.InternalError, `Failed to put Pkarr record for identifier ${identifier}: ${error.message}`);
}
// Return `true` if the DHT request was successful, otherwise return `false`.
return response.ok;
});
}
/**
* Converts a DNS packet to a DID document according to the DID DHT specification.
*
* @see {@link https://did-dht.com/#dids-as-dns-records | DID DHT Specification, § DIDs as DNS Records}
*
* @param params - The parameters to use when converting a DNS packet to a DID document.
* @param params.didUri - The DID URI of the DID document.
* @param params.dnsPacket - The DNS packet to convert to a DID document.
* @returns A Promise resolving to a {@link DidResolutionResult} object containing the DID
* document and its metadata.
*/
static fromDnsPacket(_a) {
return __awaiter(this, arguments, void 0, function* ({ didUri, dnsPacket }) {
var _b, _c, _d;
// Begin constructing the DID Document.
const didDocument = { id: didUri };
// Since the DID document is being retrieved from the DHT, it is considered published.
const didDocumentMetadata = {
published: true
};
const idLookup = new Map();
for (const answer of (_b = dnsPacket === null || dnsPacket === void 0 ? void 0 : dnsPacket.answers) !== null && _b !== void 0 ? _b : []) {
// DID DHT properties are ONLY present in DNS TXT records.
if (answer.type !== 'TXT')
continue;
// Get the DID DHT record identifier (e.g., k0, aka, did, etc.) from the DNS resource name.
const dnsRecordId = answer.name.split('.')[0].substring(1);
switch (true) {
// Process an also known as record.
case dnsRecordId.startsWith('aka'): {
// Decode the DNS TXT record data value to a string.
const data = DidDhtUtils.parseTxtDataToString(answer.data);
// Add the 'alsoKnownAs' property to the DID document.
didDocument.alsoKnownAs = data.split(VALUE_SEPARATOR);
break;
}
// Process a controller record.
case dnsRecordId.startsWith('cnt'): {
// Decode the DNS TXT record data value to a string.
const data = DidDhtUtils.parseTxtDataToString(answer.data);
// Add the 'controller' property to the DID document.
didDocument.controller = data.includes(VALUE_SEPARATOR) ? data.split(VALUE_SEPARATOR) : data;
break;
}
// Process verification methods.
case dnsRecordId.startsWith('k'): {
// Get the key type (t), Base64URL-encoded public key (k), algorithm (a), and
// optionally, controller (c) or Verification Method ID (id) from the decoded TXT record data.
const { id, t, k, c, a: parsedAlg } = DidDhtUtils.parseTxtDataToObject(answer.data);
// Convert the public key from Base64URL format to a byte array.
const publicKeyBytes = Convert.base64Url(k).toUint8Array();
// Use the key type integer to look up the cryptographic curve name.
const namedCurve = DidDhtRegisteredKeyType[Number(t)];
// Convert the public key from a byte array to JWK format.
let publicKey = yield DidDhtUtils.keyConverter(namedCurve).bytesToPublicKey({ publicKeyBytes });
publicKey.alg = parsedAlg || KeyTypeToDefaultAlgorithmMap[Number(t)];
// TOOD: when this is complete https://github.com/TBD54566975/web5-js/issues/638 then we can add this back and
// update the test vectors kid back to '0'
// if(dnsRecordId === 'k0') {
// publicKey.kid = '0';
// }
// Determine the Verification Method ID: '0' for the identity key,
// the id from the TXT Data Object, or the JWK thumbprint if an explicity Verification Method ID not defined.
const vmId = dnsRecordId === 'k0' ? '0' : id !== undefined ? id : yield computeJwkThumbprint({ jwk: publicKey });
// Initialize the `verificationMethod` array if it does not already exist.
(_c = didDocument.verificationMethod) !== null && _c !== void 0 ? _c : (didDocument.verificationMethod = []);
// Prepend the DID URI to the ID fragment to form the full verification method ID.
const methodId = `${didUri}#${vmId}`;
// Add the verification method to the DID document.
didDocument.verificationMethod.push({
id: methodId,
type: 'JsonWebKey',
controller: c !== null && c !== void 0 ? c : didUri,
publicKeyJwk: publicKey,
});
// Add a mapping from the DNS record ID (e.g., 'k0', 'k1', etc.) to the verification
// method ID (e.g., 'did:dht:...#0', etc.).
idLookup.set(dnsRecordId, methodId);
break;
}
// Process services.
case dnsRecordId.startsWith('s'): {
// Get the service ID fragment (id), type (t), service endpoint (se), and optionally,
// other properties from the decoded TXT record data.
const _e = DidDhtUtils.parseTxtDataToObject(answer.data), { id, t, se } = _e, customProperties = __rest(_e, ["id", "t", "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(([k, v]) => [k, v.includes(VALUE_SEPARATOR) ? v.split(VALUE_SEPARATOR) : v]));
// Initialize the `service` array if it does not already exist.
(_d = didDocument.service) !== null && _d !== void 0 ? _d : (didDocument.service = []);
didDocument.service.push(Object.assign(Object.assign({}, serviceProperties), { id: `${didUri}#${id}`, type: t, serviceEndpoint }));
break;
}
// Process DID DHT types.
case dnsRecordId.startsWith('typ'): {
// Decode the DNS TXT record data value to an object.
const { id: types } = DidDhtUtils.parseTxtDataToObject(answer.data);
// Add the DID DHT Registered DID Types represented as numbers to DID metadata.
didDocumentMetadata.types = types.split(VALUE_SEPARATOR).map(typeInteger => Number(typeInteger));
break;
}
// Process root record.
case dnsRecordId.startsWith('did'): {
// Helper function that maps verification relationship values to verification method IDs.
const recordIdsToMethodIds = (data) => data
.split(VALUE_SEPARATOR)
.map(dnsRecordId => idLookup.get(dnsRecordId))
.filter((id) => typeof id === 'string');
// Decode the DNS TXT record data and destructure verification relationship properties.
const { auth, asm, del, inv, agm } = DidDhtUtils.parseTxtDataToObject(answer.data);
// Add the verification relationships, if any, to the DID document.
if (auth)
didDocument.authentication = recordIdsToMethodIds(auth);
if (asm)
didDocument.assertionMethod = recordIdsToMethodIds(asm);
if (del)
didDocument.capabilityDelegation = recordIdsToMethodIds(del);
if (inv)
didDocument.capabilityInvocation = recordIdsToMethodIds(inv);
if (agm)
didDocument.keyAgreement = recordIdsToMethodIds(agm);
break;
}
}
}
return { didDocument, didDocumentMetadata, didResolutionMetadata: {} };
});
}
/**
* Converts a DID document to a DNS packet according to the DID DHT specification.
*
* @see {@link https://did-dht.com/#dids-as-dns-records | DID DHT Specification, § DIDs as DNS Records}
*
* @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.
* @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.
*/
static toDnsPacket(_a) {
return __awaiter(this, arguments, void 0, function* ({ didDocument, didMetadata, authoritativeGatewayUris, previousDidProof }) {
var _b, _c, _d, _e, _f;
const txtRecords = [];
const nsRecords = [];
const idLookup = new Map();
const serviceIds = [];
const verificationMethodIds = [];
// Add `_prv._did.` TXT record if previous DID proof is provided and valid.
if (previousDidProof !== undefined) {
const { signature, previousDid } = previousDidProof;
yield 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({
type: 'TXT',
name: '_aka._did.',
ttl: DNS_RECORD_TTL,
data: didDocument.alsoKnownAs.join(VALUE_SEPARATOR)
});
}
// Add DNS TXT records if the DID document contains a `controller` property.
if (didDocument.controller) {
const controller = Array.isArray(didDocument.controller)
? didDocument.controller.join(VALUE_SEPARATOR)
: didDocument.controller;
txtRecords.push({
type: 'TXT',
name: '_cnt._did.',
ttl: DNS_RECORD_TTL,
data: controller
});
}
// Add DNS TXT records for each verification method.
for (const [index, verificationMethod] of (_c = (_b = didDocument.verificationMethod) === null || _b === void 0 ? void 0 : _b.entries()) !== null && _c !== void 0 ? _c : []) {
const dnsRecordId = `k${index}`;
verificationMethodIds.push(dnsRecordId);
let methodId = verificationMethod.id.split('#').pop(); // Remove fragment prefix, if any.
idLookup.set(methodId, dnsRecordId);
const publicKey = verificationMethod.publicKeyJwk;
if (!((publicKey === null || publicKey === void 0 ? void 0 : publicKey.crv) && publicKey.crv in AlgorithmToKeyTypeMap)) {
throw new DidError(DidErrorCode.InvalidPublicKeyType, `Verification method '${verificationMethod.id}' contains an unsupported key type: ${(_d = publicKey === null || publicKey === void 0 ? void 0