UNPKG

@did-btcr2/method

Version:

Javascript/TypeScript reference implementation of did:btcr2 method, a censorship resistant DID Method using the Bitcoin blockchain as a Verifiable Data Registry to announce changes to the DID document. Core package of the did-btcr2-js monorepo.

531 lines (489 loc) 21.4 kB
import { getNetwork } from '@did-btcr2/bitcoin'; import { BTCR2_DID_DOCUMENT_CONTEXT, DidDocumentError, ID_PLACEHOLDER_VALUE, IdentifierTypes, INVALID_DID_DOCUMENT, JSONObject, KeyBytes, Logger } from '@did-btcr2/common'; import { CompressedSecp256k1PublicKey } from '@did-btcr2/keypair'; import { DidService, DidDocument as IIDidDocument, DidVerificationMethod as IIDidVerificationMethod } from '@web5/dids'; import { BeaconService } from '../interfaces/ibeacon.js'; import { Appendix } from './appendix.js'; import { BeaconUtils } from './beacons.js'; import { Identifier } from './identifier.js'; export const BECH32M_CHARS = ''; export const DID_REGEX = /did:btcr2:(x1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]*)/g; export type ExternalData = { id: string, verificationMethod: Array<DidVerificationMethod>, authentication?: Array<string | DidVerificationMethod>, assertionMethod?: Array<string | DidVerificationMethod>, capabilityInvocation?: Array<string | DidVerificationMethod>, capabilityDelegation?: Array<string | DidVerificationMethod>, service: Array<BeaconService> } export type VerificationRelationships = { authentication?: Array<string | DidVerificationMethod>; assertionMethod?: Array<string | DidVerificationMethod>; capabilityInvocation?: Array<string | DidVerificationMethod>; capabilityDelegation?: Array<string | DidVerificationMethod>; } export interface IDidVerificationMethod extends IIDidVerificationMethod { id: string; type: string; controller: string; publicKeyMultibase: string; secretKeyMultibase?: string | undefined; } /** * DID BTCR2 Verification Method extends the DidVerificationMethod class adding helper methods and properties * @class DidVerificationMethod * @type {DidVerificationMethod} * */ export class DidVerificationMethod implements IDidVerificationMethod { id: string; type: string; controller: string; publicKeyMultibase: string; secretKeyMultibase?: string | undefined; constructor({ id, type, controller, publicKeyMultibase, secretKeyMultibase }: IDidVerificationMethod) { this.id = id; this.type = type; this.controller = controller; this.publicKeyMultibase = publicKeyMultibase; this.secretKeyMultibase = secretKeyMultibase; if(!secretKeyMultibase){ delete this.secretKeyMultibase; } } // TODO: Add helper methods and properties } /** * BTCR2 DID Document Interface * @interface IDidDocument * @type {IDidDocument} * @extends {IIDidDocument} * @property {string} id - The identifier of the DID Document. * @property {Array<string>} [controller] - The controller of the DID Document. * @property {Array<string | JSONObject>} ['@context'] - The context of the DID Document. * @property {Array<DidVerificationMethod>} verificationMethod - The verification methods of the DID Document. * @property {Array<string | DidVerificationMethod>} [authentication] - The authentication methods of the DID Document. * @property {Array<string | DidVerificationMethod>} [assertionMethod] - The assertion methods of the DID Document. * @property {Array<string | DidVerificationMethod>} [capabilityInvocation] - The capability invocation methods of the DID Document. * @property {Array<string | DidVerificationMethod>} [capabilityDelegation] - The capability delegation methods of the DID Document. * @property {Array<BeaconService>} service - The services of the DID Document. */ export interface IDidDocument extends IIDidDocument { id: string; controller?: Array<string>; '@context'?: Array<string | JSONObject>; verificationMethod: Array<DidVerificationMethod>; authentication?: Array<string | DidVerificationMethod>; assertionMethod?: Array<string | DidVerificationMethod>; capabilityInvocation?: Array<string | DidVerificationMethod>; capabilityDelegation?: Array<string | DidVerificationMethod>; service: Array<BeaconService>; } /** * BTCR2 DID Document extends the DidDocument class adding helper methods and properties * @class DidDocument * @type {DidDocument} * @implements {IDidDocument} * @property {string} id - The identifier of the DID Document. * @property {Array<string>} [controller] - The controller of the DID Document. * @property {Array<string | JSONObject>} ['@context'] - The context of the DID Document. * @property {Array<DidVerificationMethod>} verificationMethod - The verification methods of the DID Document. * @property {Array<string | DidVerificationMethod>} [authentication] - The authentication methods of the DID Document. * @property {Array<string | DidVerificationMethod>} [assertionMethod] - The assertion methods of the DID Document. * @property {Array<string | DidVerificationMethod>} [capabilityInvocation] - The capability invocation methods of the DID Document. * @property {Array<string | DidVerificationMethod>} [capabilityDelegation] - The capability delegation methods of the DID Document. * @property {Array<BeaconService>} service - The services of the DID Document. */ export class DidDocument implements IDidDocument { id: string; controller?: Array<string>; '@context'?: Array<string | JSONObject> = BTCR2_DID_DOCUMENT_CONTEXT; verificationMethod: Array<DidVerificationMethod>; authentication?: Array<string | DidVerificationMethod>; assertionMethod?: Array<string | DidVerificationMethod>; capabilityInvocation?: Array<string | DidVerificationMethod>; capabilityDelegation?: Array<string | DidVerificationMethod>; service: Array<BeaconService>; constructor(document: IDidDocument) { console.log('Constructing DidDocument with document:', document); // Set the ID and ID type const idType = document.id.includes('k1') ? IdentifierTypes.KEY : IdentifierTypes.EXTERNAL; // Validate ID and parts for non-intermediate const isIntermediate = document.id === ID_PLACEHOLDER_VALUE; // Deconstruct the document parts for validation const { id, controller, verificationMethod: vm, service } = document; if (!isIntermediate) { if (!DidDocument.isValidId(id)) { throw new DidDocumentError(`Invalid id: ${id}`, INVALID_DID_DOCUMENT, document); } if(!DidDocument.isValidController(controller ?? [id])) { throw new DidDocumentError(`Invalid controller: ${controller}`, INVALID_DID_DOCUMENT, document); } if (!DidDocument.isValidVerificationMethods(vm)) { throw new DidDocumentError('Invalid verificationMethod: ' + vm, INVALID_DID_DOCUMENT, document); } if (!DidDocument.isValidServices(service)) { throw new DidDocumentError('Invalid service: ' + service, INVALID_DID_DOCUMENT, document); } } // Set core properties this.id = document.id; this.verificationMethod = document.verificationMethod; this.service = document.service; this['@context'] = document['@context'] || BTCR2_DID_DOCUMENT_CONTEXT; this.controller = document.controller || [this.id]; // Relationships logic based on idType if (idType === IdentifierTypes.KEY) { // auto-generate #initialKey if missing const keyRef = `${this.id}#initialKey`; this.authentication = document.authentication || [keyRef]; this.assertionMethod = document.assertionMethod || [keyRef]; this.capabilityInvocation = document.capabilityInvocation || [keyRef]; this.capabilityDelegation = document.capabilityDelegation || [keyRef]; } else { // EXTERNAL: use provided arrays, must be defined this.authentication = document.authentication; this.assertionMethod = document.assertionMethod; this.capabilityInvocation = document.capabilityInvocation; this.capabilityDelegation = document.capabilityDelegation; } // Sanitize the DID Document DidDocument.sanitize(this); // If the DID Document is not an intermediateDocument, validate it if (!isIntermediate) { DidDocument.validate(this); } else { this.validateIntermediate(); } } /** * Convert the DidDocument to a JSON object. * @returns {JSONObject} The JSON representation of the DidDocument. */ public json(): JSONObject { return { id : this.id, controller : this.controller, '@context' : this['@context'], verificationMethod : this.verificationMethod, authentication : this.authentication, assertionMethod : this.assertionMethod, capabilityInvocation : this.capabilityInvocation, capabilityDelegation : this.capabilityDelegation, service : this.service }; } /** * Create a minimal DidDocument from "k1" btcr2 identifier. * @param {string} publicKeyMultibase The public key in multibase format. * @param {Array<BeaconService>} service The beacon services to be included in the document. * @returns {DidDocument} A new DidDocument with the placeholder ID. */ public static fromKeyIdentifier( id: string, publicKeyMultibase: string, service: Array<BeaconService> ): DidDocument { // Ensure the ID is in the correct format id = id.includes('#') ? id : `${id}#initialKey`; // Create the verification method and the DidDocument const document = { id, verificationMethod : [ new DidVerificationMethod({ id, type : 'Multikey', controller : id, publicKeyMultibase }) ], service } as IDidDocument; return new DidDocument(document); } /** * Create a DidDocument from "x1" btcr2 identifier. * @param {ExternalData} data The verification methods of the DID Document. * @returns {DidDocument} A new DidDocument. */ public static fromExternalIdentifier(data: ExternalData): DidDocument { return new DidDocument(data as IDidDocument); } /** * Sanitize the DID Document by removing undefined values * @returns {DidDocument} The sanitized DID Document */ public static sanitize(doc: DidDocument): DidDocument { for (const key of Object.keys(doc)) { if (doc[key as keyof DidDocument] === undefined) { delete doc[key as keyof DidDocument]; } } return doc; } /** * Validates a DidDocument by breaking it into modular validation methods. * @param {DidDocument} didDocument The DID document to validate. * @returns {boolean} True if the DID document is valid. * @throws {DidDocumentError} If any validation check fails. */ public static isValid(didDocument: DidDocument): boolean { if (!this.isValidContext(didDocument?.['@context'])) { throw new DidDocumentError('Invalid "@context"', INVALID_DID_DOCUMENT, didDocument); } if (!this.isValidId(didDocument?.id)) { throw new DidDocumentError('Invalid "id"', INVALID_DID_DOCUMENT, didDocument); } if (!this.isValidVerificationMethods(didDocument?.verificationMethod)) { throw new DidDocumentError('Invalid "verificationMethod"', INVALID_DID_DOCUMENT, didDocument); } if (!this.isValidServices(didDocument?.service)) { throw new DidDocumentError('Invalid "service"', INVALID_DID_DOCUMENT, didDocument); } if (!this.isValidVerificationRelationships(didDocument)) { throw new DidDocumentError('Invalid verification relationships', INVALID_DID_DOCUMENT, didDocument); } return true; } /** * Validates that "@context" exists and includes correct values. * @private * @param {DidDocument['@context']} context The context to validate. * @returns {boolean} True if the context is valid. */ private static isValidContext(context: DidDocument['@context']): boolean { if(!context) return false; if(!Array.isArray(context)) return false; if(!context.every(ctx => typeof ctx === 'string' && BTCR2_DID_DOCUMENT_CONTEXT.includes(ctx))) return false; return true; } /** * Validates that the DID Document has a valid id. * @private * @param {string} id The id to validate. * @returns {boolean} True if the id is valid. */ private static isValidId(id: string): boolean { try { Identifier.decode(id); return true; } catch (error: any) { Logger.error('Invalid DID Document ID', error); return false; } } /** * Validates that the controller exists and is correctly formatted. * @param {Array<string>} controller The controller to validate. * @returns {boolean} True if the controller is valid. */ private static isValidController(controller: Array<string>): boolean { if(!controller) return false; if(!Array.isArray(controller)) return false; if(!controller.every(c => typeof c === 'string')) return false; return true; } /** * Validates that verification methods exist and are correctly formatted. * @private * @param {DidVerificationMethod[]} verificationMethod The verification methods to validate. * @returns {boolean} True if the verification methods are valid. */ private static isValidVerificationMethods(verificationMethod: IIDidVerificationMethod[]): boolean { return Array.isArray(verificationMethod) && verificationMethod.every(Appendix.isDidVerificationMethod); } /** * Validates that the DID Document has valid services. * @private * @param {DidService[]} service The services to validate. * @returns {boolean} True if the services are valid. */ private static isValidServices(service: DidService[]): boolean { return Array.isArray(service) && service.every(BeaconUtils.isBeaconService); } /** * Validates verification relationships (authentication, assertionMethod, capabilityInvocation, capabilityDelegation). * @private * @param {DidDocument} didDocument The DID Document to validate. * @returns {boolean} True if the verification relationships are valid. */ public static isValidVerificationRelationships(didDocument: DidDocument): boolean { // Define the available verification relationships const possibleVerificationRelationships: (keyof DidDocument)[] = [ 'authentication', 'assertionMethod', 'capabilityInvocation', 'capabilityDelegation' ]; // Get the DID Document keys const verificationRelationships = Object.keys(didDocument) as Array<keyof DidDocument>; // Filter the DID Document keys to only those that are in the available verification relationships const availableVerificationRelationships = possibleVerificationRelationships.filter( key => verificationRelationships.includes(key as keyof DidDocument) ) as (keyof DidDocument)[]; // Check if all available verification relationships are valid return availableVerificationRelationships.every((key) => // Check if the key exists in the DID Document didDocument[key] && // Check if the key is an array Array.isArray(didDocument[key]) && // Check that every value in the array is a string or DidVerificationMethod didDocument[key].every( entry => typeof entry === 'string' || Appendix.isDidVerificationMethod(entry) )); } /** * Validate the DID Document * @returns {DidDocument} Validated DID Document. * @throws {DidDocumentError} If the DID Document is invalid. */ public static validate(didDocument: DidDocument | IntermediateDidDocument): DidDocument { // Validate the DID Document if (didDocument.id === ID_PLACEHOLDER_VALUE) { (didDocument as IntermediateDidDocument).validateIntermediate(); } else { DidDocument.isValid(didDocument); } // Return the DID Document return didDocument; } /** * Validate the IntermediateDidDocument. * @returns {boolean} True if the IntermediateDidDocument is valid. */ public validateIntermediate(): boolean { // Validate the id if(this.id !== ID_PLACEHOLDER_VALUE) { throw new DidDocumentError('Invalid IntermediateDidDocument ID', INVALID_DID_DOCUMENT, this); } // Validate the controller if(!this.controller?.every(c => c === ID_PLACEHOLDER_VALUE)) { throw new DidDocumentError('Invalid IntermediateDidDocument controller', INVALID_DID_DOCUMENT, this); } // Validate the verificationMethod if(!this.verificationMethod.every(vm => vm.id.includes(ID_PLACEHOLDER_VALUE) && vm.controller === ID_PLACEHOLDER_VALUE)) { throw new DidDocumentError('Invalid IntermediateDidDocument verificationMethod', INVALID_DID_DOCUMENT, this); } // Validate the service if(!this.service.every(svc => svc.id.includes(ID_PLACEHOLDER_VALUE))) { throw new DidDocumentError('Invalid IntermediateDidDocument service', INVALID_DID_DOCUMENT, this); } if(!DidDocument.isValidVerificationRelationships(this)) { // Return true if all validations pass throw new DidDocumentError('Invalid IntermediateDidDocument assertionMethod', INVALID_DID_DOCUMENT, this); } return true; } /** * Convert the DidDocument to an IntermediateDidDocument. * @returns {IntermediateDidDocument} The IntermediateDidDocument representation of the DidDocument. */ public toIntermediate(): IntermediateDidDocument { if(this.id.includes('k1')) { throw new DidDocumentError('Cannot convert a key identifier to an intermediate document', INVALID_DID_DOCUMENT, this); } return new IntermediateDidDocument(this); } } /** * IntermediateDidDocument extends the DidDocument class for creating and managing intermediate DID documents. * This class is used to create a minimal DID document with a placeholder ID. * It is used in the process of creating a new DID document. * @class IntermediateDidDocument * @extends {DidDocument} */ export class IntermediateDidDocument extends DidDocument { constructor(document: JSONObject) { super(document as IDidDocument); } /** * Convert the IntermediateDidDocument to a DidDocument by replacing the placeholder value with the provided DID. * @param did The DID to replace the placeholder value in the document. * @returns {DidDocument} A new DidDocument with the placeholder value replaced by the provided DID. */ public toDidDocument(did: string): DidDocument { const stringThis = JSON.stringify(this).replaceAll(ID_PLACEHOLDER_VALUE, did); const parseThis = JSON.parse(stringThis) as IDidDocument; return new DidDocument(parseThis); } /** * Create an IntermediateDidDocument from a DidDocument by replacing the DID with a placeholder value. * @param {DidDocument} didDocument The DidDocument to convert. * @returns {IntermediateDidDocument} The IntermediateDidDocument representation of the DidDocument. */ public static fromDidDocument(didDocument: DidDocument): IntermediateDidDocument { const intermediateDocument = JSON.cloneReplace(didDocument, DID_REGEX, ID_PLACEHOLDER_VALUE) as IDidDocument; return new IntermediateDidDocument(intermediateDocument); } /** * Create a minimal IntermediateDidDocument with a placeholder ID. * @param {Array<DidVerificationMethod>} verificationMethod The public key in multibase format. * @param {VerificationRelationships} relationships The public key in multibase format. * @param {Array<BeaconService>} service The service to be included in the document. * @returns {IntermediateDidDocument} A new IntermediateDidDocument with the placeholder ID. */ public static create( verificationMethod: Array<DidVerificationMethod>, relationships: VerificationRelationships, service: Array<BeaconService> ): IntermediateDidDocument { const id = ID_PLACEHOLDER_VALUE; return new IntermediateDidDocument({ id, ...relationships, verificationMethod, service, }); } /** * Create a minimal IntermediateDidDocument from a public key. * @param {KeyBytes} publicKey The public key in bytes format. * @returns {IntermediateDidDocument} A new IntermediateDidDocument with the placeholder ID. */ public static fromPublicKey(publicKey: KeyBytes, network: string): IntermediateDidDocument { const pk = new CompressedSecp256k1PublicKey(publicKey); const id = ID_PLACEHOLDER_VALUE; const service = BeaconUtils.generateBeaconService({ id : `${id}#key-0`, publicKey : pk.compressed, network : getNetwork(network), addressType : 'p2pkh', type : 'SingletonBeacon', }); const relationships = { authentication : [`${id}#key-0`], assertionMethod : [`${id}#key-0`], capabilityInvocation : [`${id}#key-0`], capabilityDelegation : [`${id}#key-0`] }; const verificationMethod = [ { id : `${id}#key-0`, type : 'Multikey', controller : id, publicKeyMultibase : pk.multibase.encoded, } ]; return IntermediateDidDocument.create(verificationMethod, relationships, [service]); } /** * Taken an object, convert it to an IntermediateDocuemnt and then to a DidDocument. * @param {JSONObject} object The JSON object to convert. * @returns {DidDocument} The created DidDocument. */ public static fromJSON(object: JSONObject): IntermediateDidDocument { return new IntermediateDidDocument(object as IDidDocument); } } export class Document { public static isValid(didDocument: DidDocument | IntermediateDidDocument): boolean { return new DidDocument(didDocument).validateIntermediate(); } }