Skip to content

Commit

Permalink
Merge pull request #10 from aviarytech/prerotation-new-beginnings
Browse files Browse the repository at this point in the history
Adds prerotation capability
  • Loading branch information
swcurran authored Jun 7, 2024
2 parents 9586314 + 0cb8e68 commit fcc9f33
Show file tree
Hide file tree
Showing 7 changed files with 505 additions and 97 deletions.
19 changes: 14 additions & 5 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 } from "./utils";
import { bytesToHex, deriveHash } from "./utils";
import { canonicalize } from 'json-canonicalize';
import { createHash } from 'node:crypto';

Expand Down Expand Up @@ -44,9 +44,18 @@ export const documentStateIsValid = async (doc: any, proofs: any[], updateKeys:
return true;
}

export const newKeysAreValid = (options: CreateDIDInterface | UpdateDIDInterface) => {
if(!options.verificationMethods?.find(vm => vm.type === 'authentication')) {
throw new Error("DIDDoc MUST contain at least one authentication key type");
export const newKeysAreValid = (updateKeys: string[], previousnextKeyHashes: string[], nextKeyHashes: string[], previousPrerotate: boolean, prerotate: boolean) => {
if (prerotate && nextKeyHashes.length === 0) {
throw new Error(`nextKeyHashes are required if prerotation enabled`);
}
if(options.prerotate) {}
if(previousPrerotate) {
const innextKeyHashes = updateKeys.reduce((result, key) => {
const hashedKey = deriveHash(key);
return result && previousnextKeyHashes.includes(hashedKey);
}, true);
if (!innextKeyHashes) {
throw new Error(`invalid updateKeys ${updateKeys}`);
}
}
return true;
}
14 changes: 11 additions & 3 deletions src/interfaces.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,14 @@ type DIDLogEntry = [
logEntryHash: string,
versionId: number,
timestamp: string,
params: {method?: string, scid?: string, updateKeys?: string[]},
data: {value: any} | {path: DIDOperation[]},
params: {
method?: string,
scid?: string,
updateKeys?: string[],
prerotate?: boolean,
nextKeyHashes?: string[]
},
data: {value: any} | {patch: DIDOperation[]},
proof?: any
];
type DIDLog = DIDLogEntry[];
Expand Down Expand Up @@ -52,6 +58,7 @@ interface CreateDIDInterface {
verificationMethods?: VerificationMethod[];
created?: Date;
prerotate?: boolean;
nextKeyHashes?: string[];
}

interface SignDIDDocInterface {
Expand All @@ -71,8 +78,9 @@ interface UpdateDIDInterface {
alsoKnownAs?: string[];
domain?: string;
updated?: Date;
prerotate?: boolean;
deactivated?: boolean;
prerotate?: boolean;
nextKeyHashes?: string[];
}

interface DeactivateDIDInterface {
Expand Down
89 changes: 61 additions & 28 deletions src/method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,46 @@ import { documentStateIsValid, newKeysAreValid } from './assertions';


export const createDID = async (options: CreateDIDInterface): Promise<{did: string, doc: any, meta: any, log: DIDLog}> => {
newKeysAreValid(options);
if (!options.updateKeys) {
throw new Error('Update keys not supplied')
}
newKeysAreValid(options.updateKeys, [], options.nextKeyHashes ?? [], false, options.prerotate === true);
const controller = `did:${METHOD}:${options.domain}:${PLACEHOLDER}`;
const createdDate = createDate(options.created);
let {doc} = await createDIDDoc({...options, controller});
const initialLogEntry: DIDLogEntry = [
PLACEHOLDER,
1,
createdDate,
{method: PROTOCOL, scid: PLACEHOLDER, updateKeys: options.updateKeys},
{
method: PROTOCOL,
scid: PLACEHOLDER,
updateKeys: options.updateKeys,
...(options.prerotate ? {prerotate: true, nextKeyHashes: options.nextKeyHashes} : {})
},
{value: doc}
]
const {logEntryHash: initialLogEntryHash} = await deriveHash(initialLogEntry);
const initialLogEntryHash = deriveHash(initialLogEntry);
const scid = await createSCID(initialLogEntryHash);
doc = JSON.parse(JSON.stringify(doc).replaceAll(PLACEHOLDER, scid));

const logEntry: DIDLogEntry = [
scid,
1,
createdDate,
{method: PROTOCOL, scid, updateKeys: options.updateKeys},
{value: doc}
];
const {logEntryHash} = await deriveHash(logEntry);
initialLogEntry[0] = scid;
initialLogEntry[3] = JSON.parse(JSON.stringify(initialLogEntry[3]).replaceAll(PLACEHOLDER, scid));
initialLogEntry[4] = { value: doc }

const logEntryHash = deriveHash(initialLogEntry);
const signedDoc = await options.signer(doc, logEntryHash);
logEntry.push([signedDoc.proof]);
initialLogEntry.push([signedDoc.proof]);
return {
did: doc.id!,
doc,
meta: {
versionId: 1,
created: logEntry[2],
updated: logEntry[2]
created: initialLogEntry[2],
updated: initialLogEntry[2]
},
log: [
logEntry
initialLogEntry
]
}
}
Expand All @@ -63,6 +65,8 @@ export const resolveDID = async (log: DIDLog, options: {versionId?: number, vers
let previousLogEntryHash = '';
let i = 0;
let deactivated: boolean | null = null;
let prerotate = false;
let nextKeyHashes: string[] = [];
for (const entry of resolutionLog) {
if (entry[1] !== versionId + 1) {
throw new Error(`versionId '${entry[1]}' in log doesn't match expected '${versionId}'.`);
Expand All @@ -80,7 +84,10 @@ export const resolveDID = async (log: DIDLog, options: {versionId?: number, vers
newDoc = entry[4].value;
scid = entry[3].scid;
updateKeys = entry[3].updateKeys;
const {logEntryHash} = await deriveHash(
prerotate = entry[3].prerotate === true;
nextKeyHashes = entry[3].nextKeyHashes ?? [];
newKeysAreValid(updateKeys, [], nextKeyHashes, false, prerotate === true);
const logEntryHash = deriveHash(
[
PLACEHOLDER,
1,
Expand All @@ -94,7 +101,6 @@ export const resolveDID = async (log: DIDLog, options: {versionId?: number, vers
if (scid !== derivedScid) {
throw new Error(`SCID '${scid}' not derived from logEntryHash '${logEntryHash}' (scid ${derivedScid})`);
}
// const authKey = newDoc.verificationMethod.find((vm: VerificationMethod) => vm.id === entry[5][0].verificationMethod);
const verified = await documentStateIsValid(newDoc, entry[5], updateKeys);
if (!verified) {
throw new Error(`version ${versionId} failed verification of the proof.`)
Expand All @@ -106,7 +112,11 @@ export const resolveDID = async (log: DIDLog, options: {versionId?: number, vers
} else {
newDoc = jsonpatch.applyPatch(doc, entry[4].patch, false, false).newDocument;
}
const {logEntryHash} = await deriveHash([
if (entry[3].prerotate === true && (!entry[3].nextKeyHashes || entry[3].nextKeyHashes.length === 0)) {
throw new Error("prerotate enabled without nextKeyHashes");
}
newKeysAreValid(entry[3].updateKeys, nextKeyHashes, entry[3].nextKeyHashes ?? [], prerotate, entry[3].prerotate === true);
const logEntryHash = deriveHash([
previousLogEntryHash,
entry[1],
entry[2],
Expand All @@ -124,9 +134,15 @@ export const resolveDID = async (log: DIDLog, options: {versionId?: number, vers
if (entry[3].updateKeys) {
updateKeys = entry[3].updateKeys;
}
if (entry[3].deactivated) {
if (entry[3].deactivated === true) {
deactivated = true;
}
if (entry[3].prerotate === true) {
prerotate = true;
}
if (entry[3].nextKeyHashes) {
nextKeyHashes = entry[3].nextKeyHashes;
}
}
doc = clone(newDoc);
did = doc.id;
Expand All @@ -145,16 +161,30 @@ export const resolveDID = async (log: DIDLog, options: {versionId?: number, vers
if (options.versionTime || options.versionId) {
throw new Error(`DID with options ${JSON.stringify(options)} not found`);
}
return {did, doc, meta: {
versionId, created, updated, previousLogEntryHash, scid,
...(deactivated ? {deactivated}: {})
}}
return {
did,
doc,
meta: {
versionId,
created,
updated,
previousLogEntryHash,
scid,
prerotate,
nextKeyHashes,
...(deactivated ? {deactivated}: {})
}
}
}

export const updateDID = async (options: UpdateDIDInterface): Promise<{did: string, doc: any, meta: any, log: DIDLog}> => {
newKeysAreValid(options);
const {log, updateKeys, context, verificationMethods, services, alsoKnownAs, controller, domain} = options;
const {
log, updateKeys, context, verificationMethods, services, alsoKnownAs,
controller, domain, nextKeyHashes, prerotate
} = options;
let {did, doc, meta} = await resolveDID(log);
newKeysAreValid(updateKeys ?? [], meta.nextKeyHashes ?? [], nextKeyHashes ?? [], meta.prerotate === true, prerotate === true);

if (domain) {
did = `did:${METHOD}:${domain}:${log[0][3].scid}`;
}
Expand All @@ -174,10 +204,13 @@ export const updateDID = async (options: UpdateDIDInterface): Promise<{did: stri
meta.previousLogEntryHash,
meta.versionId,
meta.updated,
{...(updateKeys ? {updateKeys} : {})},
{
...(updateKeys ? {updateKeys} : {}),
...(prerotate ? {prerotate: true, nextKeyHashes} : {})
},
{patch: clone(patch)}
];
const {logEntryHash} = await deriveHash(logEntry);
const logEntryHash = deriveHash(logEntry);
logEntry[0] = logEntryHash;
const signedDoc = await options.signer(newDoc, logEntryHash);
logEntry.push([signedDoc.proof])
Expand Down Expand Up @@ -213,7 +246,7 @@ export const deactivateDID = async (options: DeactivateDIDInterface): Promise<{d
meta.updated = createDate(meta.created);
const patch = jsonpatch.compare(doc, newDoc);
const logEntry = [meta.previousLogEntryHash, meta.versionId, meta.updated, {deactivated: true}, {patch: clone(patch)}];
const {logEntryHash} = await deriveHash(logEntry);
const logEntryHash = deriveHash(logEntry);
logEntry[0] = logEntryHash;
const signedDoc = await options.signer(newDoc, logEntryHash);
logEntry.push([signedDoc.proof]);
Expand Down
4 changes: 2 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ export const createSCID = async (logEntryHash: string): Promise<string> => {
return `${logEntryHash.slice(0, 28)}`;
}

export const deriveHash = async (input: any): Promise<{logEntryHash: string}> => {
export const deriveHash = (input: any): string => {
const data = canonicalize(input);
const hash = createHash('sha256').update(data).digest();
return {logEntryHash: base32.encode(hash)};
return base32.encode(hash);
}

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

0 comments on commit fcc9f33

Please sign in to comment.