@dwn-protocol/id-sdk
Version:
SDK for accessing the features and capabilities
317 lines (275 loc) • 10.4 kB
text/typescript
import type { JwkKeyPair, PublicKeyJwk, IDCrypto } from '../crypto/index.js';
import type {
DidMethod,
DidService,
DidDocument,
PortableDid,
DidResolutionResult,
DidResolutionOptions,
VerificationRelationship,
DidKeySetVerificationMethodKey,
} from './types.js';
import { DidDht } from './dht.js';
import { EcdsaAlgorithm, EdDsaAlgorithm, Jose } from '../crypto/index.js';
import { parseDid } from './utils.js';
// for base32
import z32 from 'z32';
const SupportedCryptoKeyTypes = [
'Ed25519',
'secp256k1'
] as const;
export type DidDhtCreateOptions = {
publish?: boolean;
keySet?: DidDhtKeySet;
services?: DidService[];
}
export type DidDhtKeySet = {
verificationMethodKeys?: DidKeySetVerificationMethodKey[];
}
export class DidDhtMethod implements DidMethod {
public static methodName = 'dht';
/**
* Creates a new DID Document according to the did:dht spec.
* @param options The options to use when creating the DID Document, including whether to publish it.
* @returns A promise that resolves to a PortableDid object.
*/
public static async create(options?: DidDhtCreateOptions): Promise<PortableDid> {
const { publish = false, keySet: initialKeySet, services } = options ?? {};
// Generate missing keys, if not provided in the options.
const keySet = await this.generateKeySet({ keySet: initialKeySet });
// Get the identifier and set it.
const identityKey = keySet.verificationMethodKeys.find(key => key.publicKeyJwk.kid === '0');
const id = await this.getDidIdentifier({ key: identityKey.publicKeyJwk });
// Add all other keys to the verificationMethod and relationship arrays.
const relationshipsMap: Partial<Record<VerificationRelationship, string[]>> = {};
const verificationMethods = keySet.verificationMethodKeys.map(key => {
for (const relationship of key.relationships) {
if (relationshipsMap[relationship]) {
relationshipsMap[relationship].push(`#${key.publicKeyJwk.kid}`);
} else {
relationshipsMap[relationship] = [`#${key.publicKeyJwk.kid}`];
}
}
return {
id : `${id}#${key.publicKeyJwk.kid}`,
type : 'JsonWebKey2020',
controller : id,
publicKeyJwk : key.publicKeyJwk
};
});
// Add DID identifier to the service IDs.
services?.map(service => {
service.id = `${id}#${service.id}`;
});
// Assemble the DID Document.
const document: DidDocument = {
id,
verificationMethod: [...verificationMethods],
...relationshipsMap,
...services && { service: services }
};
// If the publish flag is set, publish the DID Document to the DHT.
if (publish) {
await this.publish({ identityKey, didDocument: document });
}
return {
did : document.id,
document : document,
keySet : keySet
};
}
/**
* Generates a JWK key pair.
* @param options The key algorithm and key ID to use.
* @returns A promise that resolves to a JwkKeyPair object.
*/
public static async generateJwkKeyPair(options: {
keyAlgorithm: typeof SupportedCryptoKeyTypes[number],
keyId?: string
}): Promise<JwkKeyPair> {
const {keyAlgorithm, keyId} = options;
let cryptoKeyPair: IDCrypto.CryptoKeyPair;
switch (keyAlgorithm) {
case 'Ed25519': {
cryptoKeyPair = await new EdDsaAlgorithm().generateKey({
algorithm : {name: 'EdDSA', namedCurve: 'Ed25519'},
extractable : true,
keyUsages : ['sign', 'verify']
});
break;
}
case 'secp256k1': {
cryptoKeyPair = await new EcdsaAlgorithm().generateKey({
algorithm : {name: 'ECDSA', namedCurve: 'secp256k1'},
extractable : true,
keyUsages : ['sign', 'verify']
});
break;
}
default: {
throw new Error(`Unsupported crypto algorithm: '${keyAlgorithm}'`);
}
}
// Convert the CryptoKeyPair to JwkKeyPair.
const jwkKeyPair = await Jose.cryptoKeyToJwkPair({keyPair: cryptoKeyPair});
// Set kid values.
if (keyId) {
jwkKeyPair.privateKeyJwk.kid = keyId;
jwkKeyPair.publicKeyJwk.kid = keyId;
} else {
// If a key ID is not specified, generate RFC 7638 JWK thumbprint.
const jwkThumbprint = await Jose.jwkThumbprint({key: jwkKeyPair.publicKeyJwk});
jwkKeyPair.privateKeyJwk.kid = jwkThumbprint;
jwkKeyPair.publicKeyJwk.kid = jwkThumbprint;
}
return jwkKeyPair;
}
/**
* Generates a key set for a DID Document.
* @param options The key set to use when generating the key set.
* @returns A promise that resolves to a DidDhtKeySet object.
*/
public static async generateKeySet(options?: {
keySet?: DidDhtKeySet
}): Promise<DidDhtKeySet> {
let { keySet = {} } = options ?? {};
// If the key set is missing a `verificationMethodKeys` array, create one.
if (!keySet.verificationMethodKeys) keySet.verificationMethodKeys = [];
// If the key set lacks an identity key (`kid: 0`), generate one.
if (!keySet.verificationMethodKeys.some(key => key.publicKeyJwk.kid === '0')) {
const identityKey = await this.generateJwkKeyPair({
keyAlgorithm : 'Ed25519',
keyId : '0'
});
keySet.verificationMethodKeys.push({
...identityKey,
relationships: ['authentication', 'assertionMethod', 'capabilityInvocation', 'capabilityDelegation']
});
}
// Generate RFC 7638 JWK thumbprints if `kid` is missing from any key.
for (const key of keySet.verificationMethodKeys) {
if (key.publicKeyJwk) key.publicKeyJwk.kid ??= await Jose.jwkThumbprint({key: key.publicKeyJwk});
if (key.privateKeyJwk) key.privateKeyJwk.kid ??= await Jose.jwkThumbprint({key: key.privateKeyJwk});
}
return keySet;
}
/**
* Gets the identifier fragment from a DID.
* @param options The key to get the identifier fragment from.
* @returns A promise that resolves to a string containing the identifier.
*/
public static async getDidIdentifier(options: {
key: PublicKeyJwk
}): Promise<string> {
const { key } = options;
const cryptoKey = await Jose.jwkToCryptoKey({ key });
const identifier = z32.encode(cryptoKey.material);
return 'did:dht:' + identifier;
}
/**
* Gets the identifier fragment from a DID.
* @param options The key to get the identifier fragment from.
* @returns A promise that resolves to a string containing the identifier fragment.
*/
public static async getDidIdentifierFragment(options: {
key: PublicKeyJwk
}): Promise<string> {
const { key } = options;
const cryptoKey = await Jose.jwkToCryptoKey({ key });
return z32.encode(cryptoKey.material);
}
/**
* Publishes a DID Document to the DHT.
* @param keySet The key set to use to sign the DHT payload.
* @param didDocument The DID Document to publish.
* @returns A boolean indicating the success of the publishing operation.
*/
public static async publish({ didDocument, identityKey }: {
didDocument: DidDocument,
identityKey: DidKeySetVerificationMethodKey
}): Promise<boolean> {
const publicCryptoKey = await Jose.jwkToCryptoKey({key: identityKey.publicKeyJwk});
const privateCryptoKey = await Jose.jwkToCryptoKey({key: identityKey.privateKeyJwk});
const isPublished = await DidDht.publishDidDocument({
keyPair: {
publicKey : publicCryptoKey,
privateKey : privateCryptoKey
},
didDocument
});
return isPublished;
}
/**
* Resolves a DID Document based on the specified options.
*
* @param options - Configuration for resolving a DID Document.
* @param options.didUrl - The DID URL to resolve.
* @param options.resolutionOptions - Optional settings for the DID resolution process as defined in the DID Core specification.
* @returns A Promise that resolves to a `DidResolutionResult`, containing the resolved DID Document and associated metadata.
*/
public static async resolve(options: {
didUrl: string,
resolutionOptions?: DidResolutionOptions
}): Promise<DidResolutionResult> {
const { didUrl, resolutionOptions: _ } = options;
// TODO: Implement resolutionOptions as defined in https://www.w3.org/TR/did-core/#did-resolution
const parsedDid = parseDid({ didUrl });
if (!parsedDid) {
return {
'@context' : 'https://w3id.org/did-resolution/v1',
didDocument : null,
didDocumentMetadata : {},
didResolutionMetadata : {
contentType : 'application/did+json',
error : 'invalidDid',
errorMessage : `Cannot parse DID: ${didUrl}`
}
};
}
if (parsedDid.method !== 'dht') {
return {
'@context' : 'https://w3id.org/did-resolution/v1',
didDocument : null,
didDocumentMetadata : {},
didResolutionMetadata : {
contentType : 'application/did+json',
error : 'methodNotSupported',
errorMessage : `Method not supported: ${parsedDid.method}`
}
};
}
let didDocument: DidDocument;
/**
* As of 5 Dec 2023, the `pkarr` library throws an error if the DID is not found. Until a
* better solution is found, catch the error and return a DID Resolution Result with an
* error message.
*/
try {
didDocument = await DidDht.getDidDocument({ did: parsedDid.did });
} catch (error: any) {
return {
'@context' : 'https://w3id.org/did-resolution/v1',
didDocument : null,
didDocumentMetadata : {},
didResolutionMetadata : {
contentType : 'application/did+json',
error : 'internalError',
errorMessage : `An unexpected error occurred while resolving DID: ${parsedDid.did}`
}
};
}
return {
'@context' : 'https://w3id.org/did-resolution/v1',
didDocument,
didDocumentMetadata : {},
didResolutionMetadata : {
contentType : 'application/did+json',
did : {
didString : parsedDid.did,
methodSpecificId : parsedDid.id,
method : parsedDid.method
}
}
};
}
}