Skip to content

Commit

Permalink
Merge pull request #23 from aviarytech/prerotate-bug
Browse files Browse the repository at this point in the history
Fix prerotation next key hashes bug.
  • Loading branch information
brianorwhatever authored Nov 28, 2024
2 parents baf3aa7 + a7b4fc3 commit 0877bff
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 70 deletions.
32 changes: 18 additions & 14 deletions src/assertions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as ed from '@noble/ed25519';
import { base58btc } from "multiformats/bases/base58";
import { bytesToHex, createSCID, deriveHash, deriveNextKeyHash, resolveVM } from "./utils";
import { bytesToHex, createSCID, deriveNextKeyHash, resolveVM } from "./utils";
import { canonicalize } from 'json-canonicalize';
import { createHash } from 'node:crypto';

Expand All @@ -9,7 +9,8 @@ const isKeyAuthorized = (verificationMethod: string, updateKeys: string[]): bool

if (verificationMethod.startsWith('did:key:')) {
const key = verificationMethod.split('did:key:')[1].split('#')[0];
return updateKeys.includes(key);
const authorized = updateKeys.includes(key);
return authorized;
}
return false;
};
Expand All @@ -26,7 +27,12 @@ const isWitnessAuthorized = (verificationMethod: string, witnesses: string[]): b

export const documentStateIsValid = async (doc: any, updateKeys: string[], witnesses: string[] = []) => {
if (process.env.IGNORE_ASSERTION_DOCUMENT_STATE_IS_VALID) return true;
const {proof: proofs, ...rest} = doc;

let {proof: proofs, ...rest} = doc;
if (!Array.isArray(proofs)) {
proofs = [proofs];
}

for (let i = 0; i < proofs.length; i++) {
const proof = proofs[i];

Expand Down Expand Up @@ -81,20 +87,18 @@ export const hashChainValid = (derivedHash: string, logEntryHash: string) => {
return derivedHash === logEntryHash;
}

export const newKeysAreValid = (updateKeys: string[], previousNextKeyHashes: string[], nextKeyHashes: string[], previousPrerotation: boolean, prerotation: boolean) => {
export const newKeysAreInNextKeys = async (updateKeys: string[], nextKeyHashes: string[], previousPrerotation: boolean, prerotation: boolean) => {
if (process.env.IGNORE_ASSERTION_NEW_KEYS_ARE_VALID) return true;
if (prerotation && nextKeyHashes.length === 0) {
throw new Error(`nextKeyHashes are required if prerotation enabled`);
}
if(previousPrerotation) {
const inNextKeyHashes = updateKeys.reduce((result, key) => {
const hashedKey = deriveNextKeyHash(key);
return result && previousNextKeyHashes.includes(hashedKey);
}, true);
if (!inNextKeyHashes) {
throw new Error(`invalid updateKeys ${updateKeys}`);

if (previousPrerotation && !prerotation) {
for (const key of updateKeys) {
const keyHash = await deriveNextKeyHash(key);
if (!nextKeyHashes.includes(keyHash)) {
throw new Error(`Invalid update key ${keyHash}. Not found in nextKeyHashes ${nextKeyHashes}`);
}
}
}

return true;
}

Expand Down
74 changes: 47 additions & 27 deletions src/method.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
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';
import { documentStateIsValid, hashChainValid, newKeysAreInNextKeys, scidIsFromHash } from './assertions';


export const createDID = async (options: CreateDIDInterface): Promise<{did: string, doc: any, meta: DIDResolutionMeta, log: DIDLog}> => {
if (!options.updateKeys) {
throw new Error('Update keys not supplied')
}
debugger;
newKeysAreValid(options.updateKeys, [], options.nextKeyHashes ?? [], false, options.prerotation === true);
if (options.prerotation && !options.nextKeyHashes) {
throw new Error('nextKeyHashes are required if prerotation enabled');
}
const controller = `did:${METHOD}:${PLACEHOLDER}:${options.domain}`;
const createdDate = createDate(options.created);
let {doc} = await createDIDDoc({...options, controller});
Expand All @@ -35,11 +36,11 @@ export const createDID = async (options: CreateDIDInterface): Promise<{did: stri
},
state: doc
};
const initialLogEntryHash = deriveHash(initialLogEntry);
const initialLogEntryHash = await deriveHash(initialLogEntry);
params.scid = await createSCID(initialLogEntryHash);
initialLogEntry.state = doc;
const prelimEntry = JSON.parse(JSON.stringify(initialLogEntry).replaceAll(PLACEHOLDER, params.scid));
const logEntryHash2 = deriveHash(prelimEntry);
const logEntryHash2 = await deriveHash(prelimEntry);
prelimEntry.versionId = `1-${logEntryHash2}`;
const signedDoc = await options.signer(prelimEntry);
let allProofs = [signedDoc.proof];
Expand Down Expand Up @@ -109,10 +110,9 @@ export const resolveDIDFromLog = async (log: DIDLog, options: {
};
let host = '';
let i = 0;
let nextKeyHashes: string[] = [];

for (const entry of resolutionLog) {
const { versionId, versionTime, parameters, state, proof } = entry;

while (i < resolutionLog.length) {
const { versionId, versionTime, parameters, state, proof } = resolutionLog[i];
const [version, entryHash] = versionId.split('-');
if (parseInt(version) !== i + 1) {
throw new Error(`version '${version}' in log doesn't match expected '${i + 1}'.`);
Expand All @@ -122,8 +122,6 @@ export const resolveDIDFromLog = async (log: DIDLog, options: {
// TODO check timestamps make sense
}
meta.updated = versionTime;

// doc patches & proof
let newDoc = state;
if (version === '1') {
meta.created = versionTime;
Expand All @@ -135,21 +133,20 @@ export const resolveDIDFromLog = async (log: DIDLog, options: {
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);
meta.nextKeyHashes = parameters.nextKeyHashes ?? [];
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);
const logEntryHash = await deriveHash(logEntry);
meta.previousLogEntryHash = logEntryHash;
if (!await scidIsFromHash(meta.scid, logEntryHash)) {
throw new Error(`SCID '${meta.scid}' not derived from logEntryHash '${logEntryHash}'`);
}
const prelimEntry = JSON.parse(JSON.stringify(logEntry).replaceAll(PLACEHOLDER, meta.scid));
const logEntryHash2 = deriveHash(prelimEntry);
const logEntryHash2 = await 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.`)
Expand All @@ -159,20 +156,31 @@ export const resolveDIDFromLog = async (log: DIDLog, options: {
if (parameters.prerotation === true && (!parameters.nextKeyHashes || parameters.nextKeyHashes.length === 0)) {
throw new Error("prerotation enabled without nextKeyHashes");
}

const newHost = newDoc.id.split(':').at(-1);
if (!meta.portable && newHost !== host) {
throw new Error("Cannot move DID: portability is disabled");
} else if (newHost !== host) {
host = newHost;
}
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(entry, meta.updateKeys, meta.witnesses);

const verified = await documentStateIsValid(resolutionLog[i], meta.updateKeys, meta.witnesses);
if (!verified) {
throw new Error(`version ${meta.versionId} failed verification of the proof.`)
}

if (!hashChainValid(`${i+1}-${entryHash}`, versionId)) {
throw new Error(`Hash chain broken at '${meta.versionId}'`);
}

await newKeysAreInNextKeys(
parameters.updateKeys ?? [],
meta.nextKeyHashes ?? [], // Use meta (previous state) for validation
meta.prerotation,
parameters.prerotation === true
);

// After all validation passes, update meta state
if (parameters.updateKeys) {
meta.updateKeys = parameters.updateKeys;
}
Expand All @@ -181,9 +189,9 @@ export const resolveDIDFromLog = async (log: DIDLog, options: {
}
if (parameters.prerotation === true) {
meta.prerotation = true;
}
if (parameters.nextKeyHashes) {
nextKeyHashes = parameters.nextKeyHashes;
meta.nextKeyHashes = parameters.nextKeyHashes || [];
} else if (parameters.nextKeyHashes) {
meta.nextKeyHashes = parameters.nextKeyHashes;
}
if (parameters.witnesses) {
meta.witnesses = parameters.witnesses;
Expand Down Expand Up @@ -222,7 +230,13 @@ export const updateDID = async (options: UpdateDIDInterface): Promise<{did: stri
controller, domain, nextKeyHashes, prerotation, witnesses, witnessThreshold
} = options;
let {did, doc, meta} = await resolveDIDFromLog(log);
newKeysAreValid(updateKeys ?? [], meta.nextKeyHashes ?? [], nextKeyHashes ?? [], meta.prerotation === true, prerotation === true);

// Check for required nextKeyHashes for prerotation
if ((meta.prerotation || prerotation === true) && (!nextKeyHashes || nextKeyHashes.length === 0)) {
throw new Error("nextKeyHashes are required if prerotation enabled");
}

await newKeysAreInNextKeys(updateKeys ?? [], nextKeyHashes ?? [], meta.prerotation, prerotation ?? false);

if (domain) {
if (!meta.portable) {
Expand All @@ -241,7 +255,12 @@ export const updateDID = async (options: UpdateDIDInterface): Promise<{did: stri
}
const params = {
...(updateKeys ? {updateKeys} : {}),
...(prerotation ? {prerotation: true, nextKeyHashes} : {}),
...(prerotation === true ? {
prerotation: true,
nextKeyHashes: nextKeyHashes || []
} : (nextKeyHashes ? {
nextKeyHashes
} : {})),
...(witnesses || meta.witnesses ? {
witnesses: witnesses || meta.witnesses,
witnessThreshold: witnesses ? witnessThreshold || witnesses.length : meta.witnessThreshold
Expand All @@ -256,7 +275,7 @@ export const updateDID = async (options: UpdateDIDInterface): Promise<{did: stri
parameters: params,
state: clone(newDoc)
};
const logEntryHash = deriveHash(logEntry);
const logEntryHash = await deriveHash(logEntry);
logEntry.versionId = `${nextVersion}-${logEntryHash}`;
const signedDoc = await options.signer(logEntry);
logEntry.proof = [signedDoc.proof];
Expand All @@ -268,6 +287,7 @@ export const updateDID = async (options: UpdateDIDInterface): Promise<{did: stri
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) {
Expand Down Expand Up @@ -306,7 +326,7 @@ export const deactivateDID = async (options: DeactivateDIDInterface): Promise<{d
parameters: {deactivated: true},
state: clone(newDoc)
};
const logEntryHash = deriveHash(logEntry);
const logEntryHash = await deriveHash(logEntry);
logEntry.versionId = `${nextVersion}-${logEntryHash}`;
const signedDoc = await options.signer(logEntry);
logEntry.proof = [signedDoc.proof];
Expand Down
19 changes: 19 additions & 0 deletions src/routes/.well-known/did.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"@context": [
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/multikey/v1"
],
"id": "did:web:localhost%3A8000",
"controller": "did:web:localhost%3A8000",
"assertionMethod": [
"did:web:localhost%3A8000#SJ4UijKX"
],
"verificationMethod": [
{
"id": "did:web:localhost%3A8000#SJ4UijKX",
"controller": "did:web:localhost%3A8000",
"type": "Multikey",
"publicKeyMultibase": "z6MkrBXGwmSFjaqxQeqMMXU6BWwrgPW2BkFXv15HSJ4UijKX"
}
]
}
2 changes: 1 addition & 1 deletion src/routes/.well-known/did.jsonl
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"versionId":"1-QmayWDYf5yveXwGRCDAQs5Nsv3jx14Q3owAL58HYe4uTtJ","versionTime":"2024-10-29T16:06:58Z","parameters":{"method":"did:tdw:0.4","scid":"QmaaoEdKWo74eu3ExBPhGyNGX8eMTYDkGJ4b85dtAMBe4H","updateKeys":["z6Mkj5xno8EhUxVDGx4rCRBxWRSFb6TgHxdXvSrEABisKwp1"],"portable":true,"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:QmaaoEdKWo74eu3ExBPhGyNGX8eMTYDkGJ4b85dtAMBe4H:localhost%3A8000","controller":"did:tdw:QmaaoEdKWo74eu3ExBPhGyNGX8eMTYDkGJ4b85dtAMBe4H:localhost%3A8000","assertionMethod":["did:tdw:QmaaoEdKWo74eu3ExBPhGyNGX8eMTYDkGJ4b85dtAMBe4H:localhost%3A8000#ABisKwp1"],"verificationMethod":[{"id":"did:tdw:QmaaoEdKWo74eu3ExBPhGyNGX8eMTYDkGJ4b85dtAMBe4H:localhost%3A8000#ABisKwp1","controller":"did:tdw:QmaaoEdKWo74eu3ExBPhGyNGX8eMTYDkGJ4b85dtAMBe4H:localhost%3A8000","type":"Multikey","publicKeyMultibase":"z6Mkj5xno8EhUxVDGx4rCRBxWRSFb6TgHxdXvSrEABisKwp1"}]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6Mkj5xno8EhUxVDGx4rCRBxWRSFb6TgHxdXvSrEABisKwp1#z6Mkj5xno8EhUxVDGx4rCRBxWRSFb6TgHxdXvSrEABisKwp1","created":"2024-10-29T16:06:58Z","proofPurpose":"assertionMethod","proofValue":"z3QDMmuuLYrpdh13zj9UrqjFzrRATdyGwyYdWT9pgts52uiRZ56jPcXiHEHqfRXLPjzrxd2UAJYyFWj3aK9oLNtf6"}]}
{"versionId":"1-QmWiLMcJcfQqXhVUB3iaL3uEyWLSP7jjvP1wzXctKU6gY3","versionTime":"2024-11-15T21:23:43Z","parameters":{"method":"did:tdw:0.4","scid":"QmTH6L1btvtXqTZMjUtjZiKvm3m8yUtMCEwJt8R7UCh3ng","updateKeys":["z6MkmMiUsxPHzmevt4rk7kM5SpXUWGTwbAevyLdVqqvtLCSR"],"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:QmTH6L1btvtXqTZMjUtjZiKvm3m8yUtMCEwJt8R7UCh3ng:localhost%3A8000","controller":"did:tdw:QmTH6L1btvtXqTZMjUtjZiKvm3m8yUtMCEwJt8R7UCh3ng:localhost%3A8000","assertionMethod":["did:tdw:QmTH6L1btvtXqTZMjUtjZiKvm3m8yUtMCEwJt8R7UCh3ng:localhost%3A8000#qqvtLCSR"],"verificationMethod":[{"id":"did:tdw:QmTH6L1btvtXqTZMjUtjZiKvm3m8yUtMCEwJt8R7UCh3ng:localhost%3A8000#qqvtLCSR","controller":"did:tdw:QmTH6L1btvtXqTZMjUtjZiKvm3m8yUtMCEwJt8R7UCh3ng:localhost%3A8000","type":"Multikey","publicKeyMultibase":"z6MkmMiUsxPHzmevt4rk7kM5SpXUWGTwbAevyLdVqqvtLCSR"}]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkmMiUsxPHzmevt4rk7kM5SpXUWGTwbAevyLdVqqvtLCSR#z6MkmMiUsxPHzmevt4rk7kM5SpXUWGTwbAevyLdVqqvtLCSR","created":"2024-11-15T21:23:43Z","proofPurpose":"assertionMethod","proofValue":"z4KbGZQd6VXPwwKn2AbSs5XFJ696ZWLqmgV4gFw33RCP4u5WxMAmJEQQJ4qB8EYWQZdR8EeqGGRR3JiPFmfNak4FF"}]}
14 changes: 8 additions & 6 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { nanoid } from 'nanoid';
import { sha256 } from 'multiformats/hashes/sha2'
import { resolveDIDFromLog } from './method';
import { join } from 'path';
import { CID } from 'multiformats/cid';
import * as raw from 'multiformats/codecs/raw';

export const readLogFromDisk = (path: string): DIDLog => {
return readLogFromString(fs.readFileSync(path, 'utf8'));
Expand Down Expand Up @@ -139,15 +141,15 @@ export const createSCID = async (logEntryHash: string): Promise<string> => {
return logEntryHash;
}

export const deriveHash = (input: any): string => {
export const deriveHash = async (input: any): Promise<string> => {
const data = canonicalize(input);
const encoder = new TextEncoder();
return base58btc.encode((sha256.digest(encoder.encode(data)) as any).bytes);
const hash = await sha256.digest(new TextEncoder().encode(data));
return base58btc.encode(hash.bytes)
}

export const deriveNextKeyHash = (input: string): string => {
const encoder = new TextEncoder();
return base58btc.encode((sha256.digest(encoder.encode(input)) as any).bytes);
export const deriveNextKeyHash = async (input: string): Promise<string> => {
const hash = await sha256.digest(new TextEncoder().encode(input));
return base58btc.encode(hash.bytes);
}

export const createDIDDoc = async (options: CreateDIDInterface): Promise<{doc: DIDDoc}> => {
Expand Down
Loading

0 comments on commit 0877bff

Please sign in to comment.