diff --git a/package.json b/package.json index db520bb..4dff70f 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,8 @@ "@noble/curves": "^1.4.2", "@noble/ed25519": "^2.1.0", "elysia": "^0.8.17", - "fast-json-patch": "^3.1.1", "json-canonicalize": "^1.0.6", "multiformats": "^13.1.0", "nanoid": "^5.0.6" } -} \ No newline at end of file +} diff --git a/src/assertions.ts b/src/assertions.ts index 874e20e..5da0178 100644 --- a/src/assertions.ts +++ b/src/assertions.ts @@ -1,6 +1,6 @@ import * as ed from '@noble/ed25519'; import { base58btc } from "multiformats/bases/base58"; -import { bytesToHex, createSCID, deriveHash, resolveVM } from "./utils"; +import { bytesToHex, createSCID, deriveHash, deriveNextKeyHash, resolveVM } from "./utils"; import { canonicalize } from 'json-canonicalize'; import { createHash } from 'node:crypto'; @@ -24,11 +24,10 @@ const isWitnessAuthorized = (verificationMethod: string, witnesses: string[]): b return false; }; -export const documentStateIsValid = async (doc: any, proofs: any[], updateKeys: string[], witnesses: string[] = []) => { +export const documentStateIsValid = async (doc: any, updateKeys: string[], witnesses: string[] = []) => { if (process.env.IGNORE_ASSERTION_DOCUMENT_STATE_IS_VALID) return true; - - let i = 0; - while(i < proofs.length) { + const {proof: proofs, ...rest} = doc; + for (let i = 0; i < proofs.length; i++) { const proof = proofs[i]; if (proof.verificationMethod.startsWith('did:key:')) { @@ -47,33 +46,32 @@ export const documentStateIsValid = async (doc: any, proofs: any[], updateKeys: throw new Error(`Unknown proof type ${proof.type}`); } if (proof.proofPurpose !== 'authentication') { - throw new Error(`Unknown proof purpose] ${proof.proofPurpose}`); + throw new Error(`Unknown proof purpose ${proof.proofPurpose}`); } if (proof.cryptosuite !== 'eddsa-jcs-2022') { throw new Error(`Unknown cryptosuite ${proof.cryptosuite}`); } + const vm = await resolveVM(proof.verificationMethod); if (!vm) { throw new Error(`Verification Method ${proof.verificationMethod} not found`); } + const publicKey = base58btc.decode(vm.publicKeyMultibase!); - if (publicKey[0] !== 237 || publicKey[1] !== 1) { - throw new Error(`multiKey doesn't include ed25519 header (0xed01)`) + if (publicKey[0] !== 0xed || publicKey[1] !== 0x01) { + throw new Error(`multiKey doesn't include ed25519 header (0xed01)`); } + const {proofValue, ...restProof} = proof; - const sig = base58btc.decode(proofValue); - const dataHash = createHash('sha256').update(canonicalize(doc)).digest(); + const signature = base58btc.decode(proofValue); + const dataHash = createHash('sha256').update(canonicalize(rest)).digest(); const proofHash = createHash('sha256').update(canonicalize(restProof)).digest(); - const input = Buffer.concat([dataHash, proofHash]); - const verified = await ed.verifyAsync( - bytesToHex(sig), - bytesToHex(input), - bytesToHex(publicKey.slice(2)) - ); + const input = Buffer.concat([proofHash, dataHash]); + + const verified = await ed.verifyAsync(Buffer.from(signature).toString('hex'), Buffer.from(input).toString('hex'), Buffer.from(publicKey.slice(2)).toString('hex')); if (!verified) { - return false; + throw new Error(`Proof ${i} failed verification`); } - i++; } return true; } @@ -90,7 +88,7 @@ export const newKeysAreValid = (updateKeys: string[], previousNextKeyHashes: str } if(previousPrerotation) { const inNextKeyHashes = updateKeys.reduce((result, key) => { - const hashedKey = deriveHash(key); + const hashedKey = deriveNextKeyHash(key); return result && previousNextKeyHashes.includes(hashedKey); }, true); if (!inNextKeyHashes) { diff --git a/src/cli.ts b/src/cli.ts index d22ac0f..3121cce 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,6 +1,6 @@ -import { createDID, resolveDID, updateDID, deactivateDID } from './method'; +import { createDID, resolveDID, updateDID, deactivateDID, resolveDIDFromLog } from './method'; import { createSigner, generateEd25519VerificationMethod } from './cryptography'; -import { getFileUrl, readLogFromDisk, writeLogToDisk, writeVerificationMethodToEnv } from './utils'; +import { fetchLogFromIdentifier, getFileUrl, readLogFromDisk, writeLogToDisk, writeVerificationMethodToEnv } from './utils'; const usage = ` Usage: bun run cli [command] [options] @@ -78,7 +78,7 @@ async function handleCreate(args: string[]) { process.exit(1); } - const authKey = await generateEd25519VerificationMethod('authentication'); + const authKey = await generateEd25519VerificationMethod(); const { did, doc, meta, log } = await createDID({ domain, signer: createSigner(authKey), @@ -114,7 +114,7 @@ async function handleResolve(args: string[]) { try { const log = await fetchLogFromIdentifier(didIdentifier); - const { did, doc, meta } = await resolveDID(log); + const { did, doc, meta } = await resolveDIDFromLog(log); console.log('Resolved DID:', did); console.log('DID Document:', JSON.stringify(doc, null, 2)); @@ -125,24 +125,6 @@ async function handleResolve(args: string[]) { } } -async function fetchLogFromIdentifier(identifier: string): Promise { - try { - const url = getFileUrl(identifier); - console.log(url, identifier) - const response = await fetch(url); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const text = await response.text(); - return text.trim().split('\n').map(line => JSON.parse(line)); - } catch (error) { - console.error('Error fetching DID log:', error); - throw error; - } -} - async function handleUpdate(args: string[]) { const options = parseOptions(args); const logFile = options['log'] as string; @@ -160,12 +142,12 @@ async function handleUpdate(args: string[]) { } const log = readLogFromDisk(logFile); - const authKey = await generateEd25519VerificationMethod('authentication'); + const authKey = await generateEd25519VerificationMethod(); const verificationMethods: VerificationMethod[] = [ authKey, ...(addVm?.map(type => ({ - type, + type: "Multikey", publicKeyMultibase: authKey.publicKeyMultibase, } as VerificationMethod)) || []) ]; @@ -203,7 +185,7 @@ async function handleDeactivate(args: string[]) { } const log = readLogFromDisk(logFile); - const authKey = await generateEd25519VerificationMethod('authentication'); + const authKey = await generateEd25519VerificationMethod(); const { did, doc, meta, log: deactivatedLog } = await deactivateDID({ log, signer: createSigner(authKey), diff --git a/src/constants.ts b/src/constants.ts index 8c9821b..30b37fa 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,6 +1,6 @@ export const PLACEHOLDER = "{SCID}"; export const METHOD = "tdw"; -export const VERSION = "0.3"; +export const VERSION = "0.4"; export const PROTOCOL = `did:${METHOD}:${VERSION}`; export const BASE_CONTEXT = [ "https://www.w3.org/ns/did/v1", diff --git a/src/cryptography.ts b/src/cryptography.ts index 436f3aa..5921b06 100644 --- a/src/cryptography.ts +++ b/src/cryptography.ts @@ -1,52 +1,51 @@ import * as ed from '@noble/ed25519'; import { edwardsToMontgomeryPub, edwardsToMontgomeryPriv } from '@noble/curves/ed25519'; -import { bytesToHex, createDate } from "./utils"; +import { createDate } from "./utils"; import { base58btc } from "multiformats/bases/base58" import { canonicalize } from 'json-canonicalize'; import { createHash } from 'node:crypto'; export const createSigner = (vm: VerificationMethod, useStatic: boolean = true) => { - return async (doc: any, challenge: string) => { + return async (doc: any) => { try { const proof: any = { type: 'DataIntegrityProof', cryptosuite: 'eddsa-jcs-2022', - verificationMethod: useStatic ? `did:key:${vm.publicKeyMultibase}` : vm.id, + verificationMethod: useStatic ? `did:key:${vm.publicKeyMultibase}#${vm.publicKeyMultibase}` : vm.id, created: createDate(), - proofPurpose: 'authentication', - challenge + proofPurpose: 'authentication' } const dataHash = createHash('sha256').update(canonicalize(doc)).digest(); const proofHash = createHash('sha256').update(canonicalize(proof)).digest(); - const input = Buffer.concat([dataHash, proofHash]); - const secretKey = base58btc.decode(vm.secretKeyMultibase!); + const input = Buffer.concat([proofHash, dataHash]); + const secretKey = base58btc.decode(vm.secretKeyMultibase!).slice(2); + const signature = await ed.signAsync(Buffer.from(input).toString('hex'), Buffer.from(secretKey).toString('hex')); - const output = await ed.signAsync(bytesToHex(input), bytesToHex(secretKey.slice(2, 34))); - - proof.proofValue = base58btc.encode(output); + proof.proofValue = base58btc.encode(signature); return {...doc, proof}; } catch (e: any) { console.error(e) - throw new Error(`Document signing failure: ${e.details}`) + throw new Error(`Document signing failure: ${e.message || e}`) } } } -export const generateEd25519VerificationMethod = async (purpose: 'authentication' | 'assertionMethod' | 'capabilityInvocation' | 'capabilityDelegation'): Promise => { +export const generateEd25519VerificationMethod = async (): Promise => { const privKey = ed.utils.randomPrivateKey(); const pubKey = await ed.getPublicKeyAsync(privKey); const publicKeyMultibase = base58btc.encode(Buffer.concat([new Uint8Array([0xed, 0x01]), pubKey])); const secretKeyMultibase = base58btc.encode(Buffer.concat([new Uint8Array([0x80, 0x26]), privKey])); return { - type: purpose, + type: "Multikey", publicKeyMultibase, - secretKeyMultibase + secretKeyMultibase, + purpose: 'authentication' }; } -export const generateX25519VerificationMethod = async (purpose: 'keyAgreement'): Promise => { +export const generateX25519VerificationMethod = async (): Promise => { const privKey = ed.utils.randomPrivateKey(); const pubKey = await ed.getPublicKeyAsync(privKey); const x25519PubKey = edwardsToMontgomeryPub(pubKey); @@ -55,8 +54,9 @@ export const generateX25519VerificationMethod = async (purpose: 'keyAgreement'): const secretKeyMultibase = base58btc.encode(Buffer.concat([new Uint8Array([0x82, 0x26]), x25519PrivKey])); return { - type: purpose, + type: "Multikey", publicKeyMultibase, - secretKeyMultibase + secretKeyMultibase, + purpose: 'keyAgreement' } } diff --git a/src/interfaces.d.ts b/src/interfaces.d.ts index e06c801..9e4197e 100644 --- a/src/interfaces.d.ts +++ b/src/interfaces.d.ts @@ -14,6 +14,7 @@ interface DIDResolutionMeta { } interface DIDDoc { + "@context"?: string | string[] | object | object[]; id?: string; controller?: string | string[]; alsoKnownAs?: string[]; @@ -26,11 +27,12 @@ interface DIDDoc { service?: ServiceEndpoint[]; } -interface DIDOperation { - op: string; - path: string; - value: any; -} +// Remove the DIDOperation interface as it's no longer needed +// interface DIDOperation { +// op: string; +// path: string; +// value: any; +// } interface DataIntegrityProof { id?: string; @@ -40,26 +42,25 @@ interface DataIntegrityProof { created: string; proofValue: string; proofPurpose: string; - challenge?: string; } -type DIDLogEntry = [ - versionId: string, - timestamp: string, - params: { - method?: string, - scid?: string, - updateKeys?: string[], - prerotation?: boolean, - nextKeyHashes?: string[], - portable?: boolean, - witnesses?: string[], - witnessThreshold?: number, - deactivated?: boolean - }, - data: {value: any} | {patch: DIDOperation[]}, - proof?: DataIntegrityProof[] -]; +interface DIDLogEntry { + versionId: string; + versionTime: string; + parameters: { + method?: string; + scid?: string; + updateKeys?: string[]; + prerotation?: boolean; + nextKeyHashes?: string[]; + portable?: boolean; + witnesses?: string[]; + witnessThreshold?: number; + deactivated?: boolean; + }; + state: DIDDoc; // Change this to specifically hold the DID document + proof?: DataIntegrityProof[]; +} type DIDLog = DIDLogEntry[]; @@ -71,7 +72,8 @@ interface ServiceEndpoint { interface VerificationMethod { id?: string; - type: 'authentication' | 'assertionMethod' | 'keyAgreement' | 'capabilityInvocation' | 'capabilityDelegation'; + type: 'Multikey'; + purpose?: 'authentication' | 'assertionMethod' | 'keyAgreement' | 'capabilityInvocation' | 'capabilityDelegation'; controller?: string; publicKeyJWK?: any; publicKeyMultibase?: string; @@ -82,7 +84,7 @@ interface VerificationMethod { interface CreateDIDInterface { domain: string; updateKeys: string[]; - signer: (doc: any, challenge: string) => Promise<{proof: any}>; + signer: (doc: any) => Promise<{proof: any}>; controller?: string; context?: string | string[]; verificationMethods?: VerificationMethod[]; @@ -102,7 +104,7 @@ interface SignDIDDocInterface { interface UpdateDIDInterface { log: DIDLog; - signer: (doc: any, challenge: string) => Promise<{proof: any}>; + signer: (doc: any) => Promise<{proof: any}>; updateKeys?: string[]; context?: string[]; controller?: string[]; @@ -120,5 +122,5 @@ interface UpdateDIDInterface { interface DeactivateDIDInterface { log: DIDLog; - signer: (doc: any, challenge: string) => Promise<{proof: any}>; + signer: (doc: any) => Promise<{proof: any}>; } diff --git a/src/method.ts b/src/method.ts index e2afdc5..2714f37 100644 --- a/src/method.ts +++ b/src/method.ts @@ -1,5 +1,4 @@ -import * as jsonpatch from 'fast-json-patch/index.mjs'; -import { clone, collectWitnessProofs, createDate, createDIDDoc, createSCID, deriveHash, findVerificationMethod, normalizeVMs } from "./utils"; +import { clone, collectWitnessProofs, createDate, createDIDDoc, createSCID, deriveHash, fetchLogFromIdentifier, findVerificationMethod, normalizeVMs } from "./utils"; import { BASE_CONTEXT, METHOD, PLACEHOLDER, PROTOCOL } from './constants'; import { documentStateIsValid, hashChainValid, newKeysAreValid, scidIsFromHash } from './assertions'; @@ -8,6 +7,7 @@ export const createDID = async (options: CreateDIDInterface): Promise<{did: stri if (!options.updateKeys) { throw new Error('Update keys not supplied') } + debugger; newKeysAreValid(options.updateKeys, [], options.nextKeyHashes ?? [], false, options.prerotation === true); const controller = `did:${METHOD}:${PLACEHOLDER}:${options.domain}`; const createdDate = createDate(options.created); @@ -26,50 +26,58 @@ export const createDID = async (options: CreateDIDInterface): Promise<{did: stri }), deactivated: false }; - const initialLogEntry: DIDLogEntry = [ - PLACEHOLDER, - createdDate, - { + const initialLogEntry: DIDLogEntry = { + versionId: PLACEHOLDER, + versionTime: createdDate, + parameters: { method: PROTOCOL, ...params }, - {value: doc} - ] + state: doc + }; const initialLogEntryHash = deriveHash(initialLogEntry); params.scid = await createSCID(initialLogEntryHash); - doc = JSON.parse(JSON.stringify(doc).replaceAll(PLACEHOLDER, params.scid)); - - initialLogEntry[0] = `1-${initialLogEntryHash}`; - initialLogEntry[2] = JSON.parse(JSON.stringify(initialLogEntry[2]).replaceAll(PLACEHOLDER, params.scid)); - initialLogEntry[3] = { value: doc } - - const signedDoc = await options.signer(doc, initialLogEntry[0]); + initialLogEntry.state = doc; + const prelimEntry = JSON.parse(JSON.stringify(initialLogEntry).replaceAll(PLACEHOLDER, params.scid)); + const logEntryHash2 = deriveHash(prelimEntry); + prelimEntry.versionId = `1-${logEntryHash2}`; + const signedDoc = await options.signer(prelimEntry); let allProofs = [signedDoc.proof]; - initialLogEntry.push(allProofs); + prelimEntry.proof = allProofs; if (options.witnesses && options.witnesses.length > 0) { - const witnessProofs = await collectWitnessProofs(options.witnesses, [initialLogEntry]); + const witnessProofs = await collectWitnessProofs(options.witnesses, [prelimEntry]); if (witnessProofs.length > 0) { allProofs = [...allProofs, ...witnessProofs]; - initialLogEntry[4] = allProofs; + prelimEntry.proof = allProofs; } } return { - did: doc.id!, - doc, + did: prelimEntry.state.id!, + doc: prelimEntry.state, meta: { - versionId: initialLogEntry[0], - created: initialLogEntry[1], - updated: initialLogEntry[1], + versionId: prelimEntry.versionId, + created: prelimEntry.versionTime, + updated: prelimEntry.versionTime, ...params }, log: [ - initialLogEntry + prelimEntry ] } } -export const resolveDID = async (log: DIDLog, options: { +export const resolveDID = async (did: string, options: { + versionNumber?: number, + versionId?: string, + versionTime?: Date, + verificationMethod?: string +} = {}): Promise<{did: string, doc: any, meta: DIDResolutionMeta}> => { + const log = await fetchLogFromIdentifier(did); + return resolveDIDFromLog(log, options); +} + +export const resolveDIDFromLog = async (log: DIDLog, options: { versionNumber?: number, versionId?: string, versionTime?: Date, @@ -79,7 +87,7 @@ export const resolveDID = async (log: DIDLog, options: { throw new Error("Cannot specify both verificationMethod and version number/id"); } const resolutionLog = clone(log); - const protocol = resolutionLog[0][2].method; + const protocol = resolutionLog[0].parameters.method; if(protocol !== PROTOCOL) { throw new Error(`'${protocol}' protocol unknown.`); } @@ -104,55 +112,51 @@ export const resolveDID = async (log: DIDLog, options: { let nextKeyHashes: string[] = []; for (const entry of resolutionLog) { - const [currentVersionId, timestamp, params, data, proof] = entry; - const [version, entryHash] = currentVersionId.split('-'); + const { versionId, versionTime, parameters, state, proof } = entry; + const [version, entryHash] = versionId.split('-'); if (parseInt(version) !== i + 1) { throw new Error(`version '${version}' in log doesn't match expected '${i + 1}'.`); } - meta.versionId = currentVersionId; - if (timestamp) { + meta.versionId = versionId; + if (versionTime) { // TODO check timestamps make sense } - meta.updated = timestamp; + meta.updated = versionTime; // doc patches & proof - let newDoc; + let newDoc = state; if (version === '1') { - meta.created = timestamp; - newDoc = data.value; + meta.created = versionTime; + newDoc = state; host = newDoc.id.split(':').at(-1); - meta.scid = params.scid; - meta.portable = params.portable ?? meta.portable; - meta.updateKeys = params.updateKeys; - meta.prerotation = params.prerotation === true; - meta.witnesses = params.witnesses || meta.witnesses; - meta.witnessThreshold = params.witnessThreshold || meta.witnessThreshold || meta.witnesses.length; - nextKeyHashes = params.nextKeyHashes ?? []; - newKeysAreValid(meta.updateKeys, [], nextKeyHashes, false, meta.prerotation === true); - const logEntryHash = deriveHash( - [ - PLACEHOLDER, - meta.created, - JSON.parse(JSON.stringify(params).replaceAll(meta.scid, PLACEHOLDER)), - {value: JSON.parse(JSON.stringify(newDoc).replaceAll(meta.scid, PLACEHOLDER))} - ] - ); + meta.scid = parameters.scid; + meta.portable = parameters.portable ?? meta.portable; + meta.updateKeys = parameters.updateKeys; + meta.prerotation = parameters.prerotation === true; + meta.witnesses = parameters.witnesses || meta.witnesses; + meta.witnessThreshold = parameters.witnessThreshold || meta.witnessThreshold || meta.witnesses.length; + nextKeyHashes = parameters.nextKeyHashes ?? []; + newKeysAreValid(meta.updateKeys, [], nextKeyHashes, false, meta.prerotation === true); + const logEntry = { + versionId: PLACEHOLDER, + versionTime: meta.created, + parameters: JSON.parse(JSON.stringify(parameters).replaceAll(meta.scid, PLACEHOLDER)), + state: JSON.parse(JSON.stringify(newDoc).replaceAll(meta.scid, PLACEHOLDER)) + }; + const logEntryHash = deriveHash(logEntry); meta.previousLogEntryHash = logEntryHash; if (!await scidIsFromHash(meta.scid, logEntryHash)) { throw new Error(`SCID '${meta.scid}' not derived from logEntryHash '${logEntryHash}'`); } - const verified = await documentStateIsValid(newDoc, proof, meta.updateKeys, meta.witnesses); + const prelimEntry = JSON.parse(JSON.stringify(logEntry).replaceAll(PLACEHOLDER, meta.scid)); + const logEntryHash2 = deriveHash(prelimEntry); + const verified = await documentStateIsValid({...prelimEntry, versionId: `1-${logEntryHash2}`, proof}, meta.updateKeys, meta.witnesses); if (!verified) { throw new Error(`version ${meta.versionId} failed verification of the proof.`) } } else { // version number > 1 - if (Object.keys(data).some((k: string) => k === 'value')) { - newDoc = data.value; - } else { - newDoc = jsonpatch.applyPatch(doc, data.patch, false, false).newDocument; - } - if (params.prerotation === true && (!params.nextKeyHashes || params.nextKeyHashes.length === 0)) { + if (parameters.prerotation === true && (!parameters.nextKeyHashes || parameters.nextKeyHashes.length === 0)) { throw new Error("prerotation enabled without nextKeyHashes"); } const newHost = newDoc.id.split(':').at(-1); @@ -161,29 +165,29 @@ export const resolveDID = async (log: DIDLog, options: { } else if (newHost !== host) { host = newHost; } - newKeysAreValid(params.updateKeys ?? [], nextKeyHashes, params.nextKeyHashes ?? [], meta.prerotation, params.prerotation === true); - if (!hashChainValid(`${i+1}-${entryHash}`, entry[0])) { + newKeysAreValid(parameters.updateKeys ?? [], nextKeyHashes, parameters.nextKeyHashes ?? [], meta.prerotation, parameters.prerotation === true); + if (!hashChainValid(`${i+1}-${entryHash}`, entry.versionId)) { throw new Error(`Hash chain broken at '${meta.versionId}'`); } - const verified = await documentStateIsValid(newDoc, proof, meta.updateKeys, meta.witnesses); + const verified = await documentStateIsValid(entry, meta.updateKeys, meta.witnesses); if (!verified) { throw new Error(`version ${meta.versionId} failed verification of the proof.`) } - if (params.updateKeys) { - meta.updateKeys = params.updateKeys; + if (parameters.updateKeys) { + meta.updateKeys = parameters.updateKeys; } - if (params.deactivated === true) { + if (parameters.deactivated === true) { meta.deactivated = true; } - if (params.prerotation === true) { + if (parameters.prerotation === true) { meta.prerotation = true; } - if (params.nextKeyHashes) { - nextKeyHashes = params.nextKeyHashes; + if (parameters.nextKeyHashes) { + nextKeyHashes = parameters.nextKeyHashes; } - if (params.witnesses) { - meta.witnesses = params.witnesses; - meta.witnessThreshold = params.witnessThreshold || params.witnesses.length; + if (parameters.witnesses) { + meta.witnesses = parameters.witnesses; + meta.witnessThreshold = parameters.witnessThreshold || parameters.witnesses.length; } } doc = clone(newDoc); @@ -198,7 +202,7 @@ export const resolveDID = async (log: DIDLog, options: { return {did, doc, meta}; } if (options.versionTime && options.versionTime > new Date(meta.updated)) { - if (resolutionLog[i+1] && options.versionTime < new Date(resolutionLog[i+1][1])) { + if (resolutionLog[i+1] && options.versionTime < new Date(resolutionLog[i+1].versionTime)) { return {did, doc, meta}; } else if(!resolutionLog[i+1]) { return {did, doc, meta}; @@ -217,14 +221,14 @@ export const updateDID = async (options: UpdateDIDInterface): Promise<{did: stri log, updateKeys, context, verificationMethods, services, alsoKnownAs, controller, domain, nextKeyHashes, prerotation, witnesses, witnessThreshold } = options; - let {did, doc, meta} = await resolveDID(log); + let {did, doc, meta} = await resolveDIDFromLog(log); newKeysAreValid(updateKeys ?? [], meta.nextKeyHashes ?? [], nextKeyHashes ?? [], meta.prerotation === true, prerotation === true); if (domain) { if (!meta.portable) { throw new Error(`Cannot move DID: portability is disabled`); } - did = `did:${METHOD}:${domain}:${log[0][2].scid}`; + did = `did:${METHOD}:${domain}:${log[0].parameters.scid}`; } const {all} = normalizeVMs(verificationMethods, did); const newDoc = { @@ -246,35 +250,34 @@ export const updateDID = async (options: UpdateDIDInterface): Promise<{did: stri const [currentVersion] = meta.versionId.split('-'); const nextVersion = parseInt(currentVersion) + 1; meta.updated = createDate(options.updated); - const patch = jsonpatch.compare(doc, newDoc); - const logEntry = [ - meta.versionId, - meta.updated, - params, - {patch: clone(patch)}, - [] as DataIntegrityProof[] - ]; + const logEntry: DIDLogEntry = { + versionId: meta.versionId, + versionTime: meta.updated, + parameters: params, + state: clone(newDoc) + }; const logEntryHash = deriveHash(logEntry); - logEntry[0] = `${nextVersion}-${logEntryHash}`; - const signedDoc = await options.signer(newDoc, logEntry[0]); - logEntry[4] = [signedDoc.proof]; - if (meta.witnesses && meta.witnesses.length > 0) { - const witnessProofs = await collectWitnessProofs(meta.witnesses, [...log, logEntry] as DIDLog); + logEntry.versionId = `${nextVersion}-${logEntryHash}`; + const signedDoc = await options.signer(logEntry); + logEntry.proof = [signedDoc.proof]; + const newMeta = { + ...meta, + versionId: logEntry.versionId, + created: meta.created, + updated: meta.updated, + previousLogEntryHash: meta.previousLogEntryHash, + ...params + }; + if (newMeta.witnesses && newMeta.witnesses.length > 0) { + const witnessProofs = await collectWitnessProofs(newMeta.witnesses, [...log, logEntry] as DIDLog); if (witnessProofs.length > 0) { - logEntry[4] = [...logEntry[4], ...witnessProofs]; + logEntry.proof = [...logEntry.proof, ...witnessProofs]; } } return { did, doc: newDoc, - meta: { - ...meta, - versionId: logEntry[0], - created: meta.created, - updated: meta.updated, - previousLogEntryHash: meta.previousLogEntryHash, - ...params - }, + meta: newMeta, log: [ ...clone(log), clone(logEntry) @@ -284,7 +287,7 @@ export const updateDID = async (options: UpdateDIDInterface): Promise<{did: stri export const deactivateDID = async (options: DeactivateDIDInterface): Promise<{did: string, doc: any, meta: DIDResolutionMeta, log: DIDLog}> => { const {log} = options; - let {did, doc, meta} = await resolveDID(log); + let {did, doc, meta} = await resolveDIDFromLog(log); const newDoc = { ...doc, authentication: [], @@ -297,23 +300,22 @@ export const deactivateDID = async (options: DeactivateDIDInterface): Promise<{d const [currentVersion] = meta.versionId.split('-'); const nextVersion = parseInt(currentVersion) + 1; meta.updated = createDate(meta.created); - const patch = jsonpatch.compare(doc, newDoc); - const logEntry: DIDLogEntry = [ - meta.versionId, - meta.updated, - {deactivated: true}, - {patch: clone(patch)} - ]; + const logEntry: DIDLogEntry = { + versionId: meta.versionId, + versionTime: meta.updated, + parameters: {deactivated: true}, + state: clone(newDoc) + }; const logEntryHash = deriveHash(logEntry); - logEntry[0] = `${nextVersion}-${logEntryHash}`; - const signedDoc = await options.signer(newDoc, logEntry[0]); - logEntry.push([signedDoc.proof]); + logEntry.versionId = `${nextVersion}-${logEntryHash}`; + const signedDoc = await options.signer(logEntry); + logEntry.proof = [signedDoc.proof]; return { did, doc: newDoc, meta: { ...meta, - versionId: logEntry[0], + versionId: logEntry.versionId, created: meta.created, updated: meta.updated, previousLogEntryHash: meta.previousLogEntryHash, diff --git a/src/routes/.well-known/did.jsonl b/src/routes/.well-known/did.jsonl index c5f59af..0561f56 100644 --- a/src/routes/.well-known/did.jsonl +++ b/src/routes/.well-known/did.jsonl @@ -1 +1 @@ -["1-QmWUentPDpw1fYJW61yet2rNG74APgdVz61SpxnsiWveqK","2024-10-03T07:45:20Z",{"method":"did:tdw:0.3","scid":"QmWUentPDpw1fYJW61yet2rNG74APgdVz61SpxnsiWveqK","updateKeys":["z6MkjxzETWiQ89KaWk8ToVzLwghqKSjAd3JFcudpbkTooi6G"],"portable":false,"prerotation":false,"nextKeyHashes":[],"witnesses":[],"witnessThreshold":0,"deactivated":false},{"value":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:tdw:QmWUentPDpw1fYJW61yet2rNG74APgdVz61SpxnsiWveqK:localhost%3A8000","controller":"did:tdw:QmWUentPDpw1fYJW61yet2rNG74APgdVz61SpxnsiWveqK:localhost%3A8000","authentication":["did:tdw:QmWUentPDpw1fYJW61yet2rNG74APgdVz61SpxnsiWveqK:localhost%3A8000#bkTooi6G"],"verificationMethod":[{"id":"did:tdw:QmWUentPDpw1fYJW61yet2rNG74APgdVz61SpxnsiWveqK:localhost%3A8000#bkTooi6G","controller":"did:tdw:QmWUentPDpw1fYJW61yet2rNG74APgdVz61SpxnsiWveqK:localhost%3A8000","type":"Multikey","publicKeyMultibase":"z6MkjxzETWiQ89KaWk8ToVzLwghqKSjAd3JFcudpbkTooi6G"}]}},[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkjxzETWiQ89KaWk8ToVzLwghqKSjAd3JFcudpbkTooi6G","created":"2024-10-03T07:45:20Z","proofPurpose":"authentication","challenge":"1-QmWUentPDpw1fYJW61yet2rNG74APgdVz61SpxnsiWveqK","proofValue":"z5eAQUmsvCnSytgsydGNUAhPv9troenHdPpR5WDt4vNWRXPQDt9wQdVtj7njiRyvUPndeYC6RExZcUubxDvqcLG7i"}]] +{"versionId":"1-QmVo8HTg988dwKhyBooHcCGHUpk23wrND9n8KAY97KQA3B","versionTime":"2024-10-16T19:49:10Z","parameters":{"method":"did:tdw:0.4","scid":"QmXToKAqZp3M6WP7HPDmEsjweTjReuAC1C8mMJAG8QNFSj","updateKeys":["z6Mkfgfqi4EWFyryzTemtL3u9v3Ueqoo2oLJbt227vs27kdp"],"portable":false,"prerotation":false,"nextKeyHashes":[],"witnesses":[],"witnessThreshold":0,"deactivated":false},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:tdw:QmXToKAqZp3M6WP7HPDmEsjweTjReuAC1C8mMJAG8QNFSj:localhost%3A8000","controller":"did:tdw:QmXToKAqZp3M6WP7HPDmEsjweTjReuAC1C8mMJAG8QNFSj:localhost%3A8000","authentication":["did:tdw:QmXToKAqZp3M6WP7HPDmEsjweTjReuAC1C8mMJAG8QNFSj:localhost%3A8000#7vs27kdp"],"verificationMethod":[{"id":"did:tdw:QmXToKAqZp3M6WP7HPDmEsjweTjReuAC1C8mMJAG8QNFSj:localhost%3A8000#7vs27kdp","controller":"did:tdw:QmXToKAqZp3M6WP7HPDmEsjweTjReuAC1C8mMJAG8QNFSj:localhost%3A8000","type":"Multikey","publicKeyMultibase":"z6Mkfgfqi4EWFyryzTemtL3u9v3Ueqoo2oLJbt227vs27kdp"}]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6Mkfgfqi4EWFyryzTemtL3u9v3Ueqoo2oLJbt227vs27kdp#z6Mkfgfqi4EWFyryzTemtL3u9v3Ueqoo2oLJbt227vs27kdp","created":"2024-10-16T19:49:10Z","proofPurpose":"authentication","proofValue":"z2dWto83Fc8iSsmpp8qKkbdJBBu82GmJGx4qrF3KjVRKtvQYEbrHKASo1JDbxXbTuoN9PnqZbY8uvDv758csXGiM6"}]} diff --git a/src/utils.ts b/src/utils.ts index 8b009c6..fe61b4d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,7 +3,7 @@ import * as base58btc from '@interop/base58-universal' import { canonicalize } from 'json-canonicalize'; import { nanoid } from 'nanoid'; import { sha256 } from 'multiformats/hashes/sha2' -import { resolveDID } from './method'; +import { resolveDIDFromLog } from './method'; import { join } from 'path'; export const readLogFromDisk = (path: string): DIDLog => { @@ -77,6 +77,23 @@ export const getFileUrl = (id: string) => { return `${baseUrl}/.well-known/did.jsonl`; } +export async function fetchLogFromIdentifier(identifier: string): Promise { + try { + const url = getFileUrl(identifier); + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const text = await response.text(); + return text.trim().split('\n').map(line => JSON.parse(line)); + } catch (error) { + console.error('Error fetching DID log:', error); + throw error; + } +} + export const createDate = (created?: Date | string) => new Date(created ?? Date.now()).toISOString().slice(0,-5)+'Z'; export function bytesToHex(bytes: Uint8Array): string { @@ -93,6 +110,11 @@ export const deriveHash = (input: any): string => { return base58btc.encode((sha256.digest(encoder.encode(data)) as any).bytes); } +export const deriveNextKeyHash = (input: string): string => { + const encoder = new TextEncoder(); + return base58btc.encode((sha256.digest(encoder.encode(input)) as any).bytes); +} + export const createDIDDoc = async (options: CreateDIDInterface): Promise<{doc: DIDDoc}> => { const {controller} = options; const {all} = normalizeVMs(options.verificationMethods, controller); @@ -119,27 +141,27 @@ export const normalizeVMs = (verificationMethod: VerificationMethod[] | undefine } const all: any = {}; const authentication = verificationMethod - ?.filter(vm => vm.type === 'authentication').map(vm => createVMID(vm, did)) + ?.filter(vm => vm.purpose === 'authentication').map(vm => createVMID(vm, did)) if (authentication && authentication?.length > 0) { all.authentication = authentication; } const assertionMethod = verificationMethod - ?.filter(vm => vm.type === 'assertionMethod').map(vm => createVMID(vm, did)) + ?.filter(vm => vm.purpose === 'assertionMethod').map(vm => createVMID(vm, did)) if (assertionMethod && assertionMethod?.length > 0) { all.assertionMethod = assertionMethod; } const keyAgreement = verificationMethod - ?.filter(vm => vm.type === 'keyAgreement').map(vm => createVMID(vm, did)); + ?.filter(vm => vm.purpose === 'keyAgreement').map(vm => createVMID(vm, did)); if (keyAgreement && keyAgreement?.length > 0) { all.keyAgreement = keyAgreement; } const capabilityDelegation = verificationMethod - ?.filter(vm => vm.type === 'capabilityDelegation').map(vm => createVMID(vm, did)); + ?.filter(vm => vm.purpose === 'capabilityDelegation').map(vm => createVMID(vm, did)); if (capabilityDelegation && capabilityDelegation?.length > 0) { all.capabilityDelegation = capabilityDelegation; } const capabilityInvocation = verificationMethod - ?.filter(vm => vm.type === 'capabilityInvocation').map(vm => createVMID(vm, did)); + ?.filter(vm => vm.purpose === 'capabilityInvocation').map(vm => createVMID(vm, did)); if (capabilityInvocation && capabilityInvocation?.length > 0) { all.capabilityInvocation = capabilityInvocation; } @@ -214,7 +236,7 @@ export const resolveVM = async (vm: string) => { const url = getFileUrl(vm.split('#')[0]); const didLog = await (await fetch(url)).text(); const logEntries: DIDLog = didLog.trim().split('\n').map(l => JSON.parse(l)); - const {doc} = await resolveDID(logEntries, {verificationMethod: vm}); + const {doc} = await resolveDIDFromLog(logEntries, {verificationMethod: vm}); return findVerificationMethod(doc, vm); } throw new Error(`Verification method ${vm} not found`); diff --git a/src/witness.ts b/src/witness.ts index 15d222b..d20d24d 100644 --- a/src/witness.ts +++ b/src/witness.ts @@ -1,5 +1,5 @@ import { createSigner } from './cryptography'; -import { resolveDID } from './method'; +import { resolveDIDFromLog } from './method'; // Parse the DID_VERIFICATION_METHODS environment variable const verificationMethods = JSON.parse(Buffer.from(process.env.DID_VERIFICATION_METHODS || 'W10=', 'base64').toString('utf8')); @@ -9,7 +9,7 @@ export async function createWitnessProof(log: DIDLog): Promise<{ proof: any } | } try { - const { did, doc, meta } = await resolveDID(log); + const { did, doc, meta } = await resolveDIDFromLog(log); // Find the corresponding verification method with secret key const fullVM = verificationMethods.find((vm: any) => meta.witnesses.includes(vm.id.split('#')[0])); @@ -18,21 +18,18 @@ export async function createWitnessProof(log: DIDLog): Promise<{ proof: any } | } const logEntry = log[log.length - 1]; - const [versionId, timestamp, params, data] = logEntry; + const { versionId, versionTime, parameters, state } = logEntry; // Create a signer using the witness verification method const signer = createSigner({ - type: 'authentication', + type: 'Multikey', id: fullVM.id, controller: fullVM.controller ?? fullVM.id.split('#')[0], publicKeyMultibase: fullVM.publicKeyMultibase, secretKeyMultibase: fullVM.secretKeyMultibase }, false); - // Sign the log entry - const signedDoc = await signer( - (data as any).value, - versionId - ); + const {proof, ...entry} = logEntry; + const signedDoc = await signer(entry); return { proof: signedDoc.proof diff --git a/test/features.test.ts b/test/features.test.ts index 97aa61a..b81c949 100644 --- a/test/features.test.ts +++ b/test/features.test.ts @@ -1,6 +1,5 @@ -import * as jsonpatch from 'fast-json-patch/index.mjs'; import { beforeAll, expect, test} from "bun:test"; -import { createDID, resolveDID, updateDID } from "../src/method"; +import { createDID, resolveDIDFromLog, updateDID } from "../src/method"; import { mock } from "bun-bagel"; import { createSigner, generateEd25519VerificationMethod } from "../src/cryptography"; import { deriveHash, createDate, clone } from "../src/utils"; @@ -16,10 +15,10 @@ let nonPortableDID: { did: string; doc: any; meta: any; log: DIDLog }; let portableDID: { did: string; doc: any; meta: any; log: DIDLog }; beforeAll(async () => { - authKey1 = await generateEd25519VerificationMethod('authentication'); - authKey2 = await generateEd25519VerificationMethod('authentication'); - authKey3 = await generateEd25519VerificationMethod('authentication'); - authKey4 = await generateEd25519VerificationMethod('authentication'); + authKey1 = await generateEd25519VerificationMethod(); + authKey2 = await generateEd25519VerificationMethod(); + authKey3 = await generateEd25519VerificationMethod(); + authKey4 = await generateEd25519VerificationMethod(); const {doc: newDoc1, log: newLog1} = await createDID({ domain: 'example.com', @@ -78,32 +77,33 @@ beforeAll(async () => { }); test("Resolve DID at time (first)", async () => { - const resolved = await resolveDID(log, {versionTime: new Date('2021-01-15T08:32:55Z')}); + console.error('log', log) + const resolved = await resolveDIDFromLog(log, {versionTime: new Date('2021-01-15T08:32:55Z')}); expect(resolved.meta.versionId.split('-')[0]).toBe('1'); }); test("Resolve DID at time (second)", async () => { - const resolved = await resolveDID(log, {versionTime: new Date('2021-02-15T08:32:55Z')}); + const resolved = await resolveDIDFromLog(log, {versionTime: new Date('2021-02-15T08:32:55Z')}); expect(resolved.meta.versionId.split('-')[0]).toBe('2'); }); test("Resolve DID at time (third)", async () => { - const resolved = await resolveDID(log, {versionTime: new Date('2021-03-15T08:32:55Z')}); + const resolved = await resolveDIDFromLog(log, {versionTime: new Date('2021-03-15T08:32:55Z')}); expect(resolved.meta.versionId.split('-')[0]).toBe('3'); }); test("Resolve DID at time (last)", async () => { - const resolved = await resolveDID(log, {versionTime: new Date('2021-04-15T08:32:55Z')}); + const resolved = await resolveDIDFromLog(log, {versionTime: new Date('2021-04-15T08:32:55Z')}); expect(resolved.meta.versionId.split('-')[0]).toBe('4'); }); test("Resolve DID at version", async () => { - const resolved = await resolveDID(log, {versionId: log[0][0]}); + const resolved = await resolveDIDFromLog(log, {versionId: log[0].versionId}); expect(resolved.meta.versionId.split('-')[0]).toBe('1'); }); test("Resolve DID latest", async () => { - const resolved = await resolveDID(log); + const resolved = await resolveDIDFromLog(log); expect(resolved.meta.versionId.split('-')[0]).toBe('4'); }); @@ -127,43 +127,43 @@ test("Require `nextKeyHashes` if prerotation enabled in Create", async () => { test("Require `nextKeyHashes` if prerotation enabled in Read (when enabled in Create)", async () => { let err; const badLog: DIDLog = [ - [ "1-5v2bjwgmeqpnuu669zd7956w1w14", "2024-06-06T08:23:06Z", { - method: "did:tdw:0.3", + { + versionId: "1-5v2bjwgmeqpnuu669zd7956w1w14", + versionTime: "2024-06-06T08:23:06Z", + parameters: { + method: "did:tdw:0.4", scid: "5v2bjwgmeqpnuu669zd7956w1w14", updateKeys: [ "z6Mkr2D4ixckmQx8tAVvXEhMuaMhzahxe61qJt7G9vYyiXiJ" ], prerotation: true, - }, { - value: { - "@context": [ "https://www.w3.org/ns/did/v1", "https://w3id.org/security/multikey/v1" - ], - id: "did:tdw:example.com:5v2bjwgmeqpnuu669zd7956w1w14", - controller: "did:tdw:example.com:5v2bjwgmeqpnuu669zd7956w1w14", - authentication: [ "did:tdw:example.com:5v2bjwgmeqpnuu669zd7956w1w14#9vYyiXiJ" - ], - verificationMethod: [ - { - id: "did:tdw:example.com:5v2bjwgmeqpnuu669zd7956w1w14#9vYyiXiJ", - controller: "did:tdw:example.com:5v2bjwgmeqpnuu669zd7956w1w14", - type: "Multikey", - publicKeyMultibase: "z6Mkr2D4ixckmQx8tAVvXEhMuaMhzahxe61qJt7G9vYyiXiJ", - } - ], - }, - }, [ + }, + state: { + "@context": [ "https://www.w3.org/ns/did/v1", "https://w3id.org/security/multikey/v1" ], + id: "did:tdw:example.com:5v2bjwgmeqpnuu669zd7956w1w14", + controller: "did:tdw:example.com:5v2bjwgmeqpnuu669zd7956w1w14", + authentication: [ "did:tdw:example.com:5v2bjwgmeqpnuu669zd7956w1w14#9vYyiXiJ" ], + verificationMethod: [ + { + id: "did:tdw:example.com:5v2bjwgmeqpnuu669zd7956w1w14#9vYyiXiJ", + controller: "did:tdw:example.com:5v2bjwgmeqpnuu669zd7956w1w14", + type: "Multikey", + publicKeyMultibase: "z6Mkr2D4ixckmQx8tAVvXEhMuaMhzahxe61qJt7G9vYyiXiJ", + } + ], + }, + proof: [ { type: "DataIntegrityProof", cryptosuite: "eddsa-jcs-2022", verificationMethod: "did:key:z6Mkr2D4ixckmQx8tAVvXEhMuaMhzahxe61qJt7G9vYyiXiJ", created: "2024-06-06T08:23:06Z", proofPurpose: "authentication", - challenge: "yfdr7xm1xf4e8eryw97r3e2yvey4gd13a93me7c6q3r7gfam3bh0", proofValue: "z4wWcu5WXftuvLtZy2jLHiyB8WJoWh8naNu4VFeGdfoBUbFie6mkQYAT2fyLXdbXBpPr7DWdgGatT6NZj7GJGmoBR", } ] - ] + } ]; try { - await resolveDID(badLog) + await resolveDIDFromLog(badLog) } catch(e) { err = e; } @@ -198,14 +198,32 @@ test("Require `nextKeyHashes` if prerotation enabled in Update", async () => { test("Require `nextKeyHashes` if prerotation enabled in Read (when enabled in Update)", async () => { let err: any; - const mockLog = createMockDIDLog([ - ['1-mock-hash', createDate(), { method: "did:tdw:0.3", scid: "test-scid" }, { value: { id: "did:tdw:example.com:test-scid" } } ], - ['2-mock-hash', createDate().toString(), {prerotation: true}, { patch: [] } ], - ['3-mock-hash', createDate().toString(), {}, { patch: [] } ], - ]); + const mockLog: DIDLog = [ + { + versionId: '1-mock-hash', + versionTime: createDate(), + parameters: { method: "did:tdw:0.4", scid: "test-scid" }, + state: { id: "did:tdw:example.com:test-scid" }, + proof: [] + }, + { + versionId: '2-mock-hash', + versionTime: createDate().toString(), + parameters: {prerotation: true}, + state: { id: "did:tdw:example.com:test-scid" }, + proof: [] + }, + { + versionId: '3-mock-hash', + versionTime: createDate().toString(), + parameters: {updateKeys: ['12312312312321']}, + state: { id: "did:tdw:example.com:test-scid" }, + proof: [] + }, + ]; try { process.env.IGNORE_ASSERTION_SCID_IS_FROM_HASH = "true"; - const {did} = await resolveDID(mockLog) + const {did} = await resolveDIDFromLog(mockLog) } catch(e) { err = e; } @@ -246,11 +264,23 @@ test("updateKeys MUST be in nextKeyHashes if prerotation enabled in Read (when e let err: any; process.env.IGNORE_ASSERTION_SCID_IS_FROM_HASH = "true"; const mockLog = createMockDIDLog([ - ['1-mock-hash', createDate(), { method: "did:tdw:0.3", scid: "test-scid", prerotation: true, nextKeyHashes: ['213123123']}, { value: { id: "did:tdw:example.com:test-scid" } } ], - ['2-mock-hash', createDate().toString(), {updateKeys: ['1213'], nextKeyHashes: ['123']}, { patch: [] } ] + { + versionId: '1-mock-hash', + versionTime: createDate(), + parameters: { method: "did:tdw:0.4", scid: "test-scid", prerotation: true, nextKeyHashes: ['213123123']}, + state: { id: "did:tdw:example.com:test-scid" }, + proof: [] + }, + { + versionId: '2-mock-hash', + versionTime: createDate().toString(), + parameters: {updateKeys: ['1213'], nextKeyHashes: ['123']}, + state: { id: "did:tdw:example.com:test-scid" }, + proof: [] + } ]); try { - const {did} = await resolveDID(mockLog); + const {did} = await resolveDIDFromLog(mockLog); } catch(e) { err = e; } @@ -296,13 +326,33 @@ test("updateKeys MUST be in nextKeyHashes if prerotation enabled in Update", asy test("updateKeys MUST be in nextKeyHashes if prerotation enabled in Read (when enabled in Update)", async () => { let err: any; process.env.IGNORE_ASSERTION_SCID_IS_FROM_HASH = "true"; - const mockLog = createMockDIDLog([ - ['1-mock-hash', createDate(), { method: "did:tdw:0.3", scid: "test-scid" }, { value: { id: "did:tdw:example.com:test-scid" } } ], - ['2-mock-hash', createDate().toString(), {prerotation: true, nextKeyHashes: ['1231']}, { patch: [] } ], - ['3-mock-hash', createDate().toString(), {updateKeys: ['12312312312321']}, { patch: [] } ], - ]); + process.env.IGNORE_ASSERTION_DOCUMENT_STATE_IS_VALID = "true"; + process.env.IGNORE_ASSERTION_HASH_CHAIN_IS_VALID = "true"; + const mockLog = [ + { + versionId: '1-mock-hash', + versionTime: createDate(), + parameters: { method: "did:tdw:0.4", scid: "test-scid" }, + state: { id: "did:tdw:example.com:test-scid" }, + proof: [] + }, + { + versionId: '2-mock-hash', + versionTime: createDate().toString(), + parameters: {prerotation: true, nextKeyHashes: ['1231']}, + state: { id: "did:tdw:example.com:test-scid" }, + proof: [] + }, + { + versionId: '3-mock-hash', + versionTime: createDate().toString(), + parameters: {updateKeys: ['12312312312321']}, + state: { id: "did:tdw:example.com:test-scid" }, + proof: [] + }, + ]; try { - const {did} = await resolveDID(mockLog); + const {did} = await resolveDIDFromLog(mockLog); } catch(e) { err = e; } @@ -310,6 +360,8 @@ test("updateKeys MUST be in nextKeyHashes if prerotation enabled in Read (when e expect(err).toBeDefined(); expect(err.message).toContain('invalid updateKeys') delete process.env.IGNORE_ASSERTION_SCID_IS_FROM_HASH; + delete process.env.IGNORE_ASSERTION_DOCUMENT_STATE_IS_VALID; + delete process.env.IGNORE_ASSERTION_HASH_CHAIN_IS_VALID; }); test("DID log with portable false should not resolve if moved", async () => { @@ -323,32 +375,19 @@ test("DID log with portable false should not resolve if moved", async () => { ...nonPortableDID.doc, id: nonPortableDID.did.replace('example.com', 'newdomain.com') }; - // Generate the patch - const patch = jsonpatch.compare(nonPortableDID.doc, newDoc); - - // Create the new log entry (without the hash and proof initially) - const newEntry = [ - `${nonPortableDID.log.length + 1}-test`, // Increment the version - newTimestamp, - { updateKeys: [authKey1.publicKeyMultibase]}, - { patch }, - { - type: "DataIntegrityProof", - cryptosuite: "eddsa-jcs-2022", - verificationMethod: authKey1.publicKeyMultibase, - created: newTimestamp, - proofPurpose: "authentication", - challenge: '1-test', - proofValue: "z5KDJTw1C2fRwTsxVzP1GXUJgapWeWxvd5VrwLucY4Pr1fwaMHDQsQwH5cPDdwSNUxiR7LHMUMpvhchDABUW8b2wB" - } - ]; + + const newEntry = { + versionId: `${nonPortableDID.log.length + 1}-test`, + versionTime: newTimestamp, + parameters: { updateKeys: [authKey1.publicKeyMultibase]}, + state: newDoc + }; const badLog: DIDLog = [ ...nonPortableDID.log as any, newEntry ]; - - await resolveDID(badLog); + await resolveDIDFromLog(badLog); } catch (e) { err = e; } @@ -368,7 +407,7 @@ test("updateDID should not allow moving a non-portable DID", async () => { updateKeys: [authKey1.publicKeyMultibase!], domain: newDomain, updated: newTimestamp, - signer: async (doc: any, challenge: string) => ({ + signer: async (doc: any) => ({ ...doc, proof: { type: "DataIntegrityProof", @@ -376,7 +415,6 @@ test("updateDID should not allow moving a non-portable DID", async () => { verificationMethod: `did:key:${authKey1.publicKeyMultibase}`, created: newTimestamp, proofPurpose: "authentication", - challenge, proofValue: "z5KDJTw1C2fRwTsxVzP1GXUJgapWeWxvd5VrwLucY4Pr1fwaMHDQsQwH5cPDdwSNUxiR7LHMUMpvhchDABUW8b2wB" } }) @@ -408,7 +446,7 @@ test("Create DID with witnesses", async () => { proofValue: "z58xkL6dbDRJjFVkBxhNHXNHFnZzZk...", proofPurpose: "authentication" } } }}); - const authKey = await generateEd25519VerificationMethod('authentication'); + const authKey = await generateEd25519VerificationMethod(); const { did, doc, meta, log } = await createDID({ domain: 'example.com', signer: createSigner(authKey), @@ -420,7 +458,7 @@ test("Create DID with witnesses", async () => { expect(meta.witnesses).toHaveLength(2); expect(meta.witnessThreshold).toBe(1); - expect(log[0][4]!.length).toBe(3); + expect(log[0].proof?.length).toBe(3); }); test("Update DID with witnesses", async () => { @@ -440,7 +478,7 @@ test("Update DID with witnesses", async () => { proofValue: "z58xkL6dbDRJjFVkBxhNHXNHFnZzZk...", proofPurpose: "authentication" } } }}); - const authKey = await generateEd25519VerificationMethod('authentication'); + const authKey = await generateEd25519VerificationMethod(); const { did, doc, meta, log } = await createDID({ domain: 'example.com', signer: createSigner(authKey), @@ -458,7 +496,7 @@ test("Update DID with witnesses", async () => { expect(updatedMeta.witnesses).toHaveLength(2); expect(updatedMeta.witnessThreshold).toBe(2); - expect(updatedLog[updatedLog.length - 1][4]!.length).toBe(1); + expect(updatedLog[updatedLog.length - 1].proof?.length).toBe(3); // 1 main proof + 2 witness proofs }); // test("Resolve DID with invalid witness proofs", async () => { @@ -468,5 +506,5 @@ test("Update DID with witnesses", async () => { // const invalidLog = [...initialLog]; // invalidLog[invalidLog.length - 1][5] = []; // Empty witness proofs -// await expect(resolveDID(invalidLog)).rejects.toThrow('Invalid witness proofs'); +// await expect(resolveDIDFromLog(invalidLog)).rejects.toThrow('Invalid witness proofs'); // }); \ No newline at end of file diff --git a/test/fixtures/not-authorized.log b/test/fixtures/not-authorized.log index f3e3319..42950d5 100644 --- a/test/fixtures/not-authorized.log +++ b/test/fixtures/not-authorized.log @@ -1,2 +1,2 @@ -["1-QmdK2cazkgA3Qxr3oUdZzZyJQ9tuqXrcELSbqbff2wgJU5","2024-10-04T06:34:25Z",{"method":"did:tdw:0.3","scid":"QmdK2cazkgA3Qxr3oUdZzZyJQ9tuqXrcELSbqbff2wgJU5","updateKeys":["z6MkpM4hdAMAFSf4oAZ3r9Pz5cSzZAEb9AxKbWNx7PEdLthC"],"portable":false,"prerotation":false,"nextKeyHashes":[],"witnesses":[],"witnessThreshold":0,"deactivated":false},{"value":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:tdw:QmdK2cazkgA3Qxr3oUdZzZyJQ9tuqXrcELSbqbff2wgJU5:example.com","controller":"did:tdw:QmdK2cazkgA3Qxr3oUdZzZyJQ9tuqXrcELSbqbff2wgJU5:example.com","authentication":["did:tdw:QmdK2cazkgA3Qxr3oUdZzZyJQ9tuqXrcELSbqbff2wgJU5:example.com#7PEdLthC"],"assertionMethod":["did:tdw:QmdK2cazkgA3Qxr3oUdZzZyJQ9tuqXrcELSbqbff2wgJU5:example.com#ko8yVqZf"],"verificationMethod":[{"id":"did:tdw:QmdK2cazkgA3Qxr3oUdZzZyJQ9tuqXrcELSbqbff2wgJU5:example.com#7PEdLthC","controller":"did:tdw:QmdK2cazkgA3Qxr3oUdZzZyJQ9tuqXrcELSbqbff2wgJU5:example.com","type":"Multikey","publicKeyMultibase":"z6MkpM4hdAMAFSf4oAZ3r9Pz5cSzZAEb9AxKbWNx7PEdLthC"},{"id":"did:tdw:QmdK2cazkgA3Qxr3oUdZzZyJQ9tuqXrcELSbqbff2wgJU5:example.com#ko8yVqZf","controller":"did:tdw:QmdK2cazkgA3Qxr3oUdZzZyJQ9tuqXrcELSbqbff2wgJU5:example.com","type":"Multikey","publicKeyMultibase":"z6MktbW9Q3SRRZH9KGgi992FAP3zsHbRNRVghua1ko8yVqZf"}]}},[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkpM4hdAMAFSf4oAZ3r9Pz5cSzZAEb9AxKbWNx7PEdLthC","created":"2024-10-04T06:34:25Z","proofPurpose":"authentication","challenge":"1-QmdK2cazkgA3Qxr3oUdZzZyJQ9tuqXrcELSbqbff2wgJU5","proofValue":"z2dUb12ES25JLmJ6STDD6LKvwS3Ro8KPHp7L1oixyitcJy8CwqK4NjEWimMmGKGz7vYz5Ryod9JL34voVXsMnFNzk"}]] -["2-Qma6VjkU24hF7mvnjEWc1eycwxnD3PQzrGHrm6que5wcTY","2024-10-04T06:34:25Z",{"witnesses":[],"witnessThreshold":0},{"patch":[{"op":"replace","path":"/verificationMethod/1/publicKeyMultibase","value":"z6MkoiGV33WxKg5eGAB92eekgropaE98PUqRW92k2GfKGfca"},{"op":"replace","path":"/verificationMethod/1/id","value":"did:tdw:QmdK2cazkgA3Qxr3oUdZzZyJQ9tuqXrcELSbqbff2wgJU5:example.com#2GfKGfca"},{"op":"replace","path":"/verificationMethod/0/publicKeyMultibase","value":"z6MksuN1RkD78bPEnbc8UdFAetgKjKHJyVu6NypGfJypQe4T"},{"op":"replace","path":"/verificationMethod/0/id","value":"did:tdw:QmdK2cazkgA3Qxr3oUdZzZyJQ9tuqXrcELSbqbff2wgJU5:example.com#fJypQe4T"},{"op":"replace","path":"/assertionMethod/0","value":"did:tdw:QmdK2cazkgA3Qxr3oUdZzZyJQ9tuqXrcELSbqbff2wgJU5:example.com#2GfKGfca"},{"op":"replace","path":"/authentication/0","value":"did:tdw:QmdK2cazkgA3Qxr3oUdZzZyJQ9tuqXrcELSbqbff2wgJU5:example.com#fJypQe4T"},{"op":"replace","path":"/controller","value":["did:tdw:QmdK2cazkgA3Qxr3oUdZzZyJQ9tuqXrcELSbqbff2wgJU5:example.com"]}]},[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MktbW9Q3SRRZH9KGgi992FAP3zsHbRNRVghua1ko8yVqZf","created":"2024-10-04T06:34:25Z","proofPurpose":"authentication","challenge":"2-Qma6VjkU24hF7mvnjEWc1eycwxnD3PQzrGHrm6que5wcTY","proofValue":"z4r9YokjSkvKAnt9DgMDN8CwvKAgYG1G5tHcydJdFGFDkpsdJio2hTGpgPHvLstBjAFeFH5x24jviuBD4u7AHxKu2"}]] +{"versionId":"1-QmX2pKGjDG6NTXFnPTHkBJ7Mmc2iwJQkvi35xEw5Qu34x7","versionTime":"2024-10-16T19:49:14Z","parameters":{"method":"did:tdw:0.4","scid":"QmdM16VTQBmiy8Errf87Vkp6XGx6CspGTrfVDkXi1BRHbb","updateKeys":["z6MkimixB3LLRtw2X8t8TachAsvP8ozrr1duC9kcgEFXSLW2"],"portable":false,"prerotation":false,"nextKeyHashes":[],"witnesses":[],"witnessThreshold":0,"deactivated":false},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:tdw:QmdM16VTQBmiy8Errf87Vkp6XGx6CspGTrfVDkXi1BRHbb:example.com","controller":"did:tdw:QmdM16VTQBmiy8Errf87Vkp6XGx6CspGTrfVDkXi1BRHbb:example.com","authentication":["did:tdw:QmdM16VTQBmiy8Errf87Vkp6XGx6CspGTrfVDkXi1BRHbb:example.com#gEFXSLW2","did:tdw:QmdM16VTQBmiy8Errf87Vkp6XGx6CspGTrfVDkXi1BRHbb:example.com#drUP7r5G"],"verificationMethod":[{"id":"did:tdw:QmdM16VTQBmiy8Errf87Vkp6XGx6CspGTrfVDkXi1BRHbb:example.com#gEFXSLW2","controller":"did:tdw:QmdM16VTQBmiy8Errf87Vkp6XGx6CspGTrfVDkXi1BRHbb:example.com","type":"Multikey","publicKeyMultibase":"z6MkimixB3LLRtw2X8t8TachAsvP8ozrr1duC9kcgEFXSLW2"},{"id":"did:tdw:QmdM16VTQBmiy8Errf87Vkp6XGx6CspGTrfVDkXi1BRHbb:example.com#drUP7r5G","controller":"did:tdw:QmdM16VTQBmiy8Errf87Vkp6XGx6CspGTrfVDkXi1BRHbb:example.com","type":"Multikey","publicKeyMultibase":"z6MkjebqPXBJ8FFog66DKBFCan4g9iPnG1dbjY5adrUP7r5G"}]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkimixB3LLRtw2X8t8TachAsvP8ozrr1duC9kcgEFXSLW2#z6MkimixB3LLRtw2X8t8TachAsvP8ozrr1duC9kcgEFXSLW2","created":"2024-10-16T19:49:14Z","proofPurpose":"authentication","proofValue":"z4or9CsM7NMyeLid5SENnbfCHGFd1hZamHvdm9iAEaoGK3bAagkxCQceNrxvQxYxCraMPWstSUrjn9XJY3WrxnzWU"}]} +{"versionId":"2-Qmbwu3Vrr3zdSjD994m1pu4BQSGZGXtMZrfuQh4GmXqUMd","versionTime":"2024-10-16T19:49:14Z","parameters":{"witnesses":[],"witnessThreshold":0},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:tdw:QmdM16VTQBmiy8Errf87Vkp6XGx6CspGTrfVDkXi1BRHbb:example.com","controller":["did:tdw:QmdM16VTQBmiy8Errf87Vkp6XGx6CspGTrfVDkXi1BRHbb:example.com"],"authentication":["did:tdw:QmdM16VTQBmiy8Errf87Vkp6XGx6CspGTrfVDkXi1BRHbb:example.com#GV4js1Ua","did:tdw:QmdM16VTQBmiy8Errf87Vkp6XGx6CspGTrfVDkXi1BRHbb:example.com#G9AguCS3"],"verificationMethod":[{"id":"did:tdw:QmdM16VTQBmiy8Errf87Vkp6XGx6CspGTrfVDkXi1BRHbb:example.com#GV4js1Ua","controller":"did:tdw:QmdM16VTQBmiy8Errf87Vkp6XGx6CspGTrfVDkXi1BRHbb:example.com","type":"Multikey","publicKeyMultibase":"z6MkgNAsxqZoFt6dbEGCcZ6NZsFtHQpdCjUgmcG3GV4js1Ua"},{"id":"did:tdw:QmdM16VTQBmiy8Errf87Vkp6XGx6CspGTrfVDkXi1BRHbb:example.com#G9AguCS3","controller":"did:tdw:QmdM16VTQBmiy8Errf87Vkp6XGx6CspGTrfVDkXi1BRHbb:example.com","type":"Multikey","publicKeyMultibase":"z6Mkvm7Wxo58rHambdmSiMTxQF7m1pLxvCK7iAMmG9AguCS3"}]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkjebqPXBJ8FFog66DKBFCan4g9iPnG1dbjY5adrUP7r5G#z6MkjebqPXBJ8FFog66DKBFCan4g9iPnG1dbjY5adrUP7r5G","created":"2024-10-16T19:49:14Z","proofPurpose":"authentication","proofValue":"z3GQFTrs8cudPLizGHWbSuMpqFoWcCxBFpCQGHLG1U9jwiDm5zQuhMyQHLJsD5sPz13k9fEBgM5EaXneTNAbc7G4a"}]} diff --git a/test/happy-path.test.ts b/test/happy-path.test.ts index f775b98..271e0cc 100644 --- a/test/happy-path.test.ts +++ b/test/happy-path.test.ts @@ -1,5 +1,5 @@ import { test, expect, beforeAll } from "bun:test"; -import { createDID, deactivateDID, resolveDID, updateDID } from "../src/method"; +import { createDID, deactivateDID, resolveDIDFromLog, updateDID } from "../src/method"; import fs from 'node:fs'; import { readLogFromDisk } from "../src/utils"; import { createVMID, deriveHash } from "../src/utils"; @@ -34,7 +34,7 @@ const writeFilesToDisk = (_log: DIDLog, _doc: any, version: number) => { const testResolveVersion = async (version: number) => { const log = readLogFromDisk(logFile); - const {did: resolvedDID, doc: resolvedDoc, meta} = await resolveDID(log, {versionNumber: version}); + const {did: resolvedDID, doc: resolvedDoc, meta} = await resolveDIDFromLog(log, {versionNumber: version}); if(verboseMode) { console.log(`Resolved DID Document: ${version}`, resolvedDID, resolvedDoc); @@ -46,7 +46,7 @@ const testResolveVersion = async (version: number) => { } beforeAll(async () => { - currentAuthKey = await generateEd25519VerificationMethod('authentication'); + currentAuthKey = await generateEd25519VerificationMethod(); }); test("Create DID (2 keys + domain)", async () => { @@ -57,7 +57,7 @@ test("Create DID (2 keys + domain)", async () => { portable: true, verificationMethods: [ currentAuthKey!, - await generateEd25519VerificationMethod('assertionMethod') + await generateEd25519VerificationMethod() ]}); did = newDID; currentAuthKey!.controller = did; @@ -70,10 +70,10 @@ test("Create DID (2 keys + domain)", async () => { expect(newDoc.id).toBe(newDID); expect(newLog.length).toBe(1); - expect(newLog[0][0]).toBe(meta.versionId); - expect(newLog[0][1]).toBe(meta.created); - expect(newLog[0][2].method).toBe(`did:${METHOD}:0.3`); - expect(newLog[0][2].portable).toBe(true); + expect(newLog[0].versionId).toBe(meta.versionId); + expect(newLog[0].versionTime).toBe(meta.created); + expect(newLog[0].parameters.method).toBe(`did:${METHOD}:0.4`); + expect(newLog[0].parameters.portable).toBe(true); writeFilesToDisk(newLog, newDoc, 1); }); @@ -83,7 +83,7 @@ test("Resolve DID version 1", async () => { }); test("Update DID (2 keys, 1 service, change domain)", async () => { - const nextAuthKey = await generateEd25519VerificationMethod('authentication'); + const nextAuthKey = await generateEd25519VerificationMethod(); const didLog = readLogFromDisk(logFile); const context = ["https://identity.foundation/linked-vp/contexts/v1"]; @@ -96,7 +96,7 @@ test("Update DID (2 keys, 1 service, change domain)", async () => { domain: 'localhost%3A8000', verificationMethods: [ nextAuthKey, - await generateEd25519VerificationMethod('assertionMethod') + await generateEd25519VerificationMethod() ], services: [ { @@ -124,9 +124,9 @@ test("Resolve DID version 2", async () => { }); test("Update DID (3 keys, 2 services)", async () => { - const nextAuthKey = await generateEd25519VerificationMethod('authentication'); + const nextAuthKey = await generateEd25519VerificationMethod(); const didLog = readLogFromDisk(logFile); - const {doc} = await resolveDID(didLog); + const {doc} = await resolveDIDFromLog(didLog); const {did: updatedDID, doc: updatedDoc, meta, log: updatedLog} = await updateDID({ @@ -136,8 +136,8 @@ test("Update DID (3 keys, 2 services)", async () => { context: [...doc['@context'], 'https://didcomm.org/messaging/v2'], verificationMethods: [ nextAuthKey, - await generateEd25519VerificationMethod('assertionMethod'), - await generateX25519VerificationMethod('keyAgreement') + await generateEd25519VerificationMethod(), + await generateX25519VerificationMethod() ], services: [ ...doc.service, @@ -171,9 +171,9 @@ test("Resolve DID version 3", async () => { }); test("Update DID (add alsoKnownAs)", async () => { - const nextAuthKey = await generateEd25519VerificationMethod('authentication'); + const nextAuthKey = await generateEd25519VerificationMethod(); const didLog = readLogFromDisk(logFile); - const {doc} = await resolveDID(didLog); + const {doc} = await resolveDIDFromLog(didLog); const {did: updatedDID, doc: updatedDoc, meta, log: updatedLog} = await updateDID({ @@ -183,8 +183,8 @@ test("Update DID (add alsoKnownAs)", async () => { context: doc['@context'], verificationMethods: [ nextAuthKey, - await generateEd25519VerificationMethod('assertionMethod'), - await generateX25519VerificationMethod('keyAgreement') + await generateEd25519VerificationMethod(), + await generateX25519VerificationMethod() ], services: doc.service, alsoKnownAs: ['did:web:example.com'] @@ -203,9 +203,9 @@ test("Resolve DID version 4", async () => { test("Update DID (add external controller)", async () => { let didLog = readLogFromDisk(logFile); - const {doc} = await resolveDID(didLog); - const nextAuthKey = await generateEd25519VerificationMethod('authentication'); - const externalAuthKey = await generateEd25519VerificationMethod('authentication'); + const {doc} = await resolveDIDFromLog(didLog); + const nextAuthKey = await generateEd25519VerificationMethod(); + const externalAuthKey = await generateEd25519VerificationMethod(); externalAuthKey.controller = externalAuthKey.publicKeyMultibase; const {did: updatedDID, doc: updatedDoc, meta, log: updatedLog} = await updateDID({ @@ -230,7 +230,7 @@ test("Update DID (add external controller)", async () => { didLog = [...updatedLog]; expect(updatedDID).toBe(did); expect(updatedDoc.controller).toContain(externalAuthKey.controller); - expect(updatedDoc.authentication[1].slice(-6)).toBe(externalAuthKey.controller!.slice(-6)); + expect(updatedDoc.authentication[1]).toContain(externalAuthKey.controller!.slice(-6)); expect(updatedDoc.verificationMethod[1].controller).toBe(externalAuthKey.controller); expect(meta.versionId.split('-')[0]).toBe("5"); @@ -245,10 +245,10 @@ test("Resolve DID version 5", async () => { test("Update DID (enable prerotation)", async () => { let didLog = readLogFromDisk(logFile); - const {doc} = await resolveDID(didLog); + const {doc} = await resolveDIDFromLog(didLog); - const nextAuthKey = await generateEd25519VerificationMethod('authentication'); - const nextNextAuthKey = await generateEd25519VerificationMethod('authentication'); + const nextAuthKey = await generateEd25519VerificationMethod(); + const nextNextAuthKey = await generateEd25519VerificationMethod(); const nextNextKeyHash = deriveHash(nextNextAuthKey.publicKeyMultibase); const {did: updatedDID, doc: updatedDoc, meta, log: updatedLog} = await updateDID({ @@ -286,7 +286,7 @@ test("Resolve DID version 6", async () => { test("Deactivate DID", async () => { let didLog = readLogFromDisk(logFile); - const {doc} = await resolveDID(didLog); + const {doc} = await resolveDIDFromLog(didLog); const {did: updatedDID, doc: updatedDoc, meta, log: updatedLog} = await deactivateDID({ log: didLog, diff --git a/test/interop.test.ts b/test/interop.test.ts index 15fc4ad..a7e14a6 100644 --- a/test/interop.test.ts +++ b/test/interop.test.ts @@ -1,18 +1,15 @@ import { describe, expect, test } from "bun:test"; -import { resolveDID } from "../src"; +import { resolveDID } from "../src/method"; -describe("did:tdw normative tests", async () => { +describe("did:tdw interoperability tests", async () => { test("anywhy.ca", async () => { - const didLog: DIDLog = [ - ["1-Qmabn3Tj2Bs5RMtbBuKPGwaAzPZbm9amhSzVxPw3HpZJqw", "2024-07-31T16:46:31Z", {"prerotation": false, "updateKeys": ["z6Mku46DLkSH6kHudmyjpiPktTfe7a9tcqipnFnLdSLnFiHx"], "method": "did:tdw:0.3", "scid": "QmbnhbtbN9NLCSL9TGpDLVChPd9KUw6QEJqQ9XWn3ZZ9qT"}, {"value": {"@context": ["https://www.w3.org/ns/did/v1", "https://w3id.org/security/multikey/v1"], "id": "did:tdw:QmbnhbtbN9NLCSL9TGpDLVChPd9KUw6QEJqQ9XWn3ZZ9qT:anywhy.ca"}}, [{"type": "DataIntegrityProof", "cryptosuite": "eddsa-jcs-2022", "verificationMethod": "did:key:z6Mku46DLkSH6kHudmyjpiPktTfe7a9tcqipnFnLdSLnFiHx#z6Mku46DLkSH6kHudmyjpiPktTfe7a9tcqipnFnLdSLnFiHx", "created": "2024-07-31T16:46:31Z", "proofPurpose": "authentication", "challenge": "1-Qmabn3Tj2Bs5RMtbBuKPGwaAzPZbm9amhSzVxPw3HpZJqw", "proofValue": "z5YDUr8bhCwWBn64rrWXoNCVRwS25rrT96G8DW1g8deJRhTy6LgBkL6DAxJNUhLHhx6gTEdV2abdDFHVEcajTotzh"}]], - ["2-QmYDqCcA5HT18AEAchLwfP9xkab3Kz1YL9VPYNqXsAJ358", "2024-07-31T16:46:32Z", {}, {"patch": [{"op": "add", "path": "/authentication", "value": ["did:tdw:QmbnhbtbN9NLCSL9TGpDLVChPd9KUw6QEJqQ9XWn3ZZ9qT:anywhy.ca#z6MkiP9rSWJivm6nFf7YH2tYtfqyEUoKn5P6G5VC3NdCoTpZ"]}, {"op": "add", "path": "/verificationMethod", "value": [{"id": "did:tdw:QmbnhbtbN9NLCSL9TGpDLVChPd9KUw6QEJqQ9XWn3ZZ9qT:anywhy.ca#z6MkiP9rSWJivm6nFf7YH2tYtfqyEUoKn5P6G5VC3NdCoTpZ", "controller": "did:tdw:QmbnhbtbN9NLCSL9TGpDLVChPd9KUw6QEJqQ9XWn3ZZ9qT:anywhy.ca", "type": "Multikey", "publicKeyMultibase": "z6MkiP9rSWJivm6nFf7YH2tYtfqyEUoKn5P6G5VC3NdCoTpZ"}]}, {"op": "add", "path": "/service", "value": [{"id": "did:tdw:QmbnhbtbN9NLCSL9TGpDLVChPd9KUw6QEJqQ9XWn3ZZ9qT:anywhy.ca#domain", "type": "LinkedDomains", "serviceEndpoint": "https://anywhy.ca"}, {"id": "did:tdw:QmbnhbtbN9NLCSL9TGpDLVChPd9KUw6QEJqQ9XWn3ZZ9qT:anywhy.ca#whois", "type": "LinkedVerifiablePresentation", "serviceEndpoint": "https://anywhy.ca/.well-known/whois.vc"}]}, {"op": "add", "path": "/assertionMethod", "value": ["did:tdw:QmbnhbtbN9NLCSL9TGpDLVChPd9KUw6QEJqQ9XWn3ZZ9qT:anywhy.ca#z6MkiP9rSWJivm6nFf7YH2tYtfqyEUoKn5P6G5VC3NdCoTpZ"]}, {"op": "add", "path": "/@context/2", "value": "https://identity.foundation/.well-known/did-configuration/v1"}, {"op": "add", "path": "/@context/3", "value": "https://identity.foundation/linked-vp/contexts/v1"}]}, [{"type": "DataIntegrityProof", "cryptosuite": "eddsa-jcs-2022", "verificationMethod": "did:key:z6Mku46DLkSH6kHudmyjpiPktTfe7a9tcqipnFnLdSLnFiHx#z6Mku46DLkSH6kHudmyjpiPktTfe7a9tcqipnFnLdSLnFiHx", "created": "2024-07-31T16:46:32Z", "proofPurpose": "authentication", "challenge": "2-QmYDqCcA5HT18AEAchLwfP9xkab3Kz1YL9VPYNqXsAJ358", "proofValue": "zghVCLhCwHZaBZrBtqJLLCedwV4cGuokKtWVwwXoYFFZcE48gsQPQq5HZ8qmvacqCaeKtMFJ2oXcBqHmpqkXJeNj"}]] - ]; - - const res = await resolveDID(didLog); + const did = "did:tdw:QmRyZ5pcm12CmMs4UhuN3h3Vr7Z7qRqHkxjNzUQpygPe25:anywhy.ca"; - expect(res.did).toBe("did:tdw:QmbnhbtbN9NLCSL9TGpDLVChPd9KUw6QEJqQ9XWn3ZZ9qT:anywhy.ca"); - expect(res.meta.versionId).toBe("2-QmYDqCcA5HT18AEAchLwfP9xkab3Kz1YL9VPYNqXsAJ358"); - expect(res.meta.prerotation).toBe(false); - expect(res.meta.portable).toBe(false); + const {did: resolvedDID, meta} = await resolveDID(did); + + expect(resolvedDID).toBe(did); + expect(meta.versionId).toBe("3-QmYKJo2xvahkwKFKzEybDMzSWGYcPVsX2eGYZTYqY678iK"); + expect(meta.prerotation).toBe(true); + expect(meta.portable).toBe(false); }) }); diff --git a/test/must.test.ts b/test/must.test.ts index 8d1d076..f3f8021 100644 --- a/test/must.test.ts +++ b/test/must.test.ts @@ -1,5 +1,5 @@ import { beforeAll, describe, expect, test } from "bun:test"; -import { createDID, deactivateDID, resolveDID, updateDID } from "../src/method"; +import { createDID, deactivateDID, resolveDIDFromLog, updateDID } from "../src/method"; import { createSigner, generateEd25519VerificationMethod } from "../src/cryptography"; @@ -9,7 +9,7 @@ describe("did:tdw normative tests", async () => { let authKey1: VerificationMethod; beforeAll(async () => { - authKey1 = await generateEd25519VerificationMethod('authentication'); + authKey1 = await generateEd25519VerificationMethod(); const { doc, log } = await createDID({ domain: 'example.com', @@ -24,7 +24,7 @@ describe("did:tdw normative tests", async () => { }); test("Resolve MUST process the DID Log correctly (positive)", async () => { - const resolved = await resolveDID(newLog1); + const resolved = await resolveDIDFromLog(newLog1); expect(resolved.meta.versionId.split('-')[0]).toBe("1"); }); @@ -32,7 +32,7 @@ describe("did:tdw normative tests", async () => { let err; const malformedLog = "malformed log content"; try { - await resolveDID(malformedLog as any); + await resolveDIDFromLog(malformedLog as any); } catch (e) { err = e; } @@ -40,7 +40,7 @@ describe("did:tdw normative tests", async () => { }); test("Update implementation MUST generate a correct DID Entry (positive)", async () => { - const authKey2 = await generateEd25519VerificationMethod('authentication'); + const authKey2 = await generateEd25519VerificationMethod(); const { doc: updatedDoc, log: updatedLog } = await updateDID({ log: newLog1, signer: createSigner(authKey2), @@ -50,8 +50,8 @@ describe("did:tdw normative tests", async () => { updated: new Date('2024-02-01T08:32:55Z') }); - expect(updatedLog[1][0]).toBeDefined(); - expect(updatedLog[1][0].split('-')[0]).toBe("2"); + expect(updatedLog[1].versionId).toBeDefined(); + expect(updatedLog[1].versionId.split('-')[0]).toBe("2"); }); test("Resolver encountering 'deactivated': true MUST return deactivated in metadata (positive)", async () => { @@ -59,12 +59,12 @@ describe("did:tdw normative tests", async () => { log: newLog1, signer: createSigner(authKey1) }); - const resolved = await resolveDID(updatedLog); + const resolved = await resolveDIDFromLog(updatedLog); expect(resolved.meta.deactivated).toBe(true); }); test("Resolver encountering 'deactivated': false MUST return deactivated in metadata (negative)", async () => { - const resolved = await resolveDID(newLog1); + const resolved = await resolveDIDFromLog(newLog1); expect(resolved.meta.deactivated).toBeFalse(); }); }); diff --git a/test/not-so-happy-path.test.ts b/test/not-so-happy-path.test.ts index 6040356..9cee3b8 100644 --- a/test/not-so-happy-path.test.ts +++ b/test/not-so-happy-path.test.ts @@ -1,11 +1,11 @@ import { expect, test } from "bun:test"; import { readLogFromDisk, writeLogToDisk } from "../src/utils"; -import { createDID, resolveDID, updateDID } from "../src/method"; +import { createDID, resolveDIDFromLog, updateDID } from "../src/method"; import { createSigner, generateEd25519VerificationMethod } from "../src/cryptography"; test("Update with wrong key fails resolution", async () => { - const authKey = await generateEd25519VerificationMethod('authentication'); - const assertionKey = await generateEd25519VerificationMethod('assertionMethod'); + const authKey = await generateEd25519VerificationMethod(); + const assertionKey = await generateEd25519VerificationMethod(); const {doc: newDoc, log: newLog} = await createDID({ domain: 'example.com', updateKeys: [authKey.publicKeyMultibase!], @@ -23,8 +23,8 @@ test("Update with wrong key fails resolution", async () => { signer: createSigner(assertionKey as any), context: newDoc['@context'], verificationMethods: [ - await generateEd25519VerificationMethod('authentication'), - await generateEd25519VerificationMethod('assertionMethod'), + await generateEd25519VerificationMethod(), + await generateEd25519VerificationMethod(), ] }); @@ -38,11 +38,11 @@ test("Update with wrong key fails resolution", async () => { const badLog = readLogFromDisk('./test/fixtures/not-authorized.log'); let result; try { - result = await resolveDID(badLog); + result = await resolveDIDFromLog(badLog); } catch(e: any) { err = e; } expect(result).toBeUndefined(); expect(err.message).toContain('is not authorized to update.') -}); \ No newline at end of file +}); diff --git a/test/resolve.test.ts b/test/resolve.test.ts index 7e03b60..a86ee0e 100644 --- a/test/resolve.test.ts +++ b/test/resolve.test.ts @@ -1,17 +1,17 @@ import { describe, expect, test, beforeAll } from "bun:test"; -import { createDID, resolveDID, updateDID } from "../src/method"; +import { createDID, resolveDIDFromLog, updateDID } from "../src/method"; import { createSigner, generateEd25519VerificationMethod, generateX25519VerificationMethod } from "../src/cryptography"; import { clone } from "../src/utils"; -describe("resolveDID with verificationMethod", () => { +describe("resolveDIDFromLog with verificationMethod", () => { let initialDID: string; let fullLog: DIDLog; let authKey1: VerificationMethod, authKey2: VerificationMethod, keyAgreementKey: VerificationMethod; beforeAll(async () => { - authKey1 = await generateEd25519VerificationMethod('authentication'); - authKey2 = await generateEd25519VerificationMethod('authentication'); - keyAgreementKey = await generateX25519VerificationMethod('keyAgreement'); + authKey1 = await generateEd25519VerificationMethod(); + authKey2 = await generateEd25519VerificationMethod(); + keyAgreementKey = await generateX25519VerificationMethod(); // Create initial DID const { did, log } = await createDID({ @@ -47,7 +47,7 @@ describe("resolveDID with verificationMethod", () => { test("Resolve DID with initial authentication key", async () => { const vmId = `${initialDID}#${authKey1.publicKeyMultibase!.slice(-8)}`; - const { doc, meta } = await resolveDID(fullLog, { verificationMethod: vmId }); + const { doc, meta } = await resolveDIDFromLog(fullLog, { verificationMethod: vmId }); expect(doc.verificationMethod).toHaveLength(1); expect(doc.verificationMethod[0].publicKeyMultibase).toBe(authKey1.publicKeyMultibase); @@ -56,7 +56,7 @@ describe("resolveDID with verificationMethod", () => { test("Resolve DID with second authentication key", async () => { const vmId = `${initialDID}#${authKey2.publicKeyMultibase!.slice(-8)}`; - const { doc, meta } = await resolveDID(fullLog, { verificationMethod: vmId }); + const { doc, meta } = await resolveDIDFromLog(fullLog, { verificationMethod: vmId }); expect(doc.verificationMethod).toHaveLength(2); expect(doc.verificationMethod[1].publicKeyMultibase).toBe(authKey2.publicKeyMultibase); @@ -65,7 +65,7 @@ describe("resolveDID with verificationMethod", () => { test("Resolve DID with keyAgreement key", async () => { const vmId = `${initialDID}#${keyAgreementKey.publicKeyMultibase!.slice(-8)}`; - const { doc, meta } = await resolveDID(fullLog, { verificationMethod: vmId }); + const { doc, meta } = await resolveDIDFromLog(fullLog, { verificationMethod: vmId }); expect(doc.verificationMethod).toHaveLength(3); expect(doc.verificationMethod[2].publicKeyMultibase).toBe(keyAgreementKey.publicKeyMultibase); @@ -74,12 +74,12 @@ describe("resolveDID with verificationMethod", () => { test("Resolve DID with non-existent verification method", async () => { const vmId = `${initialDID}#nonexistent`; - await expect(resolveDID(fullLog, { verificationMethod: vmId })).rejects.toThrow("DID with options"); + await expect(resolveDIDFromLog(fullLog, { verificationMethod: vmId })).rejects.toThrow("DID with options"); }); test("Resolve DID with verification method and version time", async () => { const vmId = `${initialDID}#${authKey2.publicKeyMultibase!.slice(-8)}`; - const { doc, meta } = await resolveDID(fullLog, { + const { doc, meta } = await resolveDIDFromLog(fullLog, { verificationMethod: vmId, versionTime: new Date('2023-02-15T00:00:00Z') }); @@ -94,7 +94,7 @@ describe("resolveDID with verificationMethod", () => { let error: Error | null = null; try { - await resolveDID(fullLog, { + await resolveDIDFromLog(fullLog, { verificationMethod: vmId, versionNumber: 2 }); diff --git a/test/utils.ts b/test/utils.ts index f3cbb70..0efa27e 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -3,13 +3,13 @@ import { deriveHash } from '../src/utils'; export function createMockDIDLog(entries: Partial[]): DIDLog { return entries.map((entry, index) => { const versionNumber = index + 1; - const mockEntry: DIDLogEntry = [ - `${versionNumber}-${deriveHash(entry)}`, - entry[1] || new Date().toISOString(), - entry[2] || {}, - entry[3] || { value: {} }, - entry[4] || [] - ]; + const mockEntry: DIDLogEntry = { + versionId: entry.versionId || `${versionNumber}-${deriveHash(entry)}`, + versionTime: entry.versionTime || new Date().toISOString(), + parameters: entry.parameters || {}, + state: entry.state || {}, + proof: entry.proof || [] + }; return mockEntry; }); } diff --git a/test/witness.test.ts b/test/witness.test.ts index 10cda0d..b4677f0 100644 --- a/test/witness.test.ts +++ b/test/witness.test.ts @@ -1,17 +1,33 @@ import { beforeAll, describe, expect, test } from "bun:test"; -import { createDID, resolveDID, updateDID } from "../src/method"; +import { createDID, resolveDIDFromLog, updateDID } from "../src/method"; import { createSigner, generateEd25519VerificationMethod } from "../src/cryptography"; let WITNESS_SCID = ""; const WITNESS_SERVER_URL = "http://localhost:8000"; // Update this to match your witness server URL const WITNESS_DOMAIN = WITNESS_SERVER_URL.split('//')[1].replace(':', '%3A'); -const getWitnessDID = async () => { +const getWitnessDIDLog = async () => { try { const response = await fetch(`${WITNESS_SERVER_URL}/.well-known/did.jsonl`); - return response.ok && (await response.json()); + const logFile = await response.text(); + + // Split the logFile by newlines and filter out any empty lines + const logEntries = logFile.split('\n').filter(line => line.trim() !== ''); + + // Parse each non-empty line as JSON + const parsedLog = logEntries.map(line => { + try { + return JSON.parse(line); + } catch (error) { + console.error(`Error parsing log entry: ${line}`); + return null; + } + }).filter(entry => entry !== null); + + return parsedLog; } catch (error) { - return false; + console.error('Error fetching or parsing witness DID log:', error); + return []; } } @@ -41,9 +57,9 @@ const runWitnessTests = async () => { let initialDID: { did: string; doc: any; meta: any; log: DIDLog }; beforeAll(async () => { - authKey = await generateEd25519VerificationMethod('authentication'); - const didLog = await getWitnessDID(); - const {did, meta} = await resolveDID([didLog] as DIDLog); + authKey = await generateEd25519VerificationMethod(); + const didLog = await getWitnessDIDLog(); + const {did, meta} = await resolveDIDFromLog(didLog as DIDLog); WITNESS_SCID = meta.scid; console.log(`Witness DID ${did} found`); }); @@ -58,16 +74,16 @@ const runWitnessTests = async () => { witnesses: [`did:tdw:${WITNESS_SCID}:${WITNESS_DOMAIN}`], witnessThreshold: 1 }); - const resolved = await resolveDID(initialDID.log); + const resolved = await resolveDIDFromLog(initialDID.log); expect(resolved.did).toBe(initialDID.did); expect(initialDID.meta.witnesses).toHaveLength(1); expect(initialDID.meta.witnessThreshold).toBe(1); - expect(initialDID.log[0][4]).toHaveLength(2); // Controller proof + witness proof + expect(initialDID.log[0].proof).toHaveLength(2); // Controller proof + witness proof }); test("Update DID with witness", async () => { - const newAuthKey = await generateEd25519VerificationMethod('authentication'); + const newAuthKey = await generateEd25519VerificationMethod(); const updatedDID = await updateDID({ log: initialDID.log, signer: createSigner(authKey), @@ -77,7 +93,7 @@ const runWitnessTests = async () => { expect(updatedDID.meta.witnesses).toHaveLength(1); expect(updatedDID.meta.witnessThreshold).toBe(1); - expect(updatedDID.log[updatedDID.log.length - 1][4]).toHaveLength(2); // Controller proof + witness proof + expect(updatedDID.log[updatedDID.log.length - 1].proof).toHaveLength(2); // Controller proof + witness proof }); test("Witness signing with environment variable key", async () => { @@ -114,4 +130,4 @@ const runWitnessTests = async () => { }); }; -runWitnessTests(); \ No newline at end of file +runWitnessTests();