factom-identity-lib
Version:
Library to read and update Factom identities
616 lines (532 loc) • 25.4 kB
JavaScript
const assert = require('chai').assert,
sinon = require('sinon'),
{ randomBytes } = require('crypto'),
{ FactomCli, Entry } = require('factom'),
{ generateRandomIdentityKeyPair } = require('../../src/app/key-helpers'),
{
generateIdentityChain,
generateIdentityKeyReplacementEntry
} = require('../../src/app/identity-struct'),
{ IdentityInformationRetriever } = require('../../src/app/identity-information-retriever');
describe('Compute active keys at block height', function() {
it('Should get initial set of keys', async function() {
const chainId = randomBytes(32).toString('hex');
const initialPublicKeys = getRandomKeys().map(k => k.public);
const entries = [[getIdentityFirstEntry(initialPublicKeys)]];
const blockContexts = getBlockContexts([[1]]);
const cli = getMockedCli(chainId, entries, blockContexts);
const retriever = new IdentityInformationRetriever(cli);
const activeKeys = await retriever.getActiveKeysAtHeight(chainId, 99999);
assert.deepStrictEqual(activeKeys, initialPublicKeys);
});
it('Should reject invalid initial set of keys', async function() {
const chainId = randomBytes(32).toString('hex');
const initialPublicKeys = getRandomKeys().map(k => k.public);
initialPublicKeys.push('NOT A VALID PUBLIC KEY');
const entries = [[getIdentityFirstEntry(initialPublicKeys)]];
const blockContexts = getBlockContexts([[1]]);
const cli = getMockedCli(chainId, entries, blockContexts);
const retriever = new IdentityInformationRetriever(cli);
try {
await retriever.getActiveKeysAtHeight(chainId, 99999);
} catch (e) {
assert.instanceOf(e, Error);
return;
}
assert.fail('Should have thrown');
});
it('Should handle 2 key rotations in the same block', async function() {
const chainId = randomBytes(32).toString('hex');
const initialKeys = getRandomKeys();
const initialPublicKeys = initialKeys.map(k => k.public);
const newPublicIdKey = getRandomKeys(1)[0].public;
const newPublicIdKey2 = getRandomKeys(1)[0].public;
const replacement = generateIdentityKeyReplacementEntry(
chainId,
initialPublicKeys[1],
newPublicIdKey,
initialKeys[0]
);
const replacement2 = generateIdentityKeyReplacementEntry(
chainId,
initialPublicKeys[2],
newPublicIdKey2,
initialKeys[0]
);
const entries = [[getIdentityFirstEntry(initialPublicKeys), replacement, replacement2]];
const blockContexts = getBlockContexts([[1, 5, 5]]);
const cli = getMockedCli(chainId, entries, blockContexts);
const retriever = new IdentityInformationRetriever(cli);
const keys = await retriever.getActiveKeysAtHeight(chainId, 8);
const rotatedKeys = [...initialPublicKeys];
rotatedKeys[1] = newPublicIdKey;
rotatedKeys[2] = newPublicIdKey2;
assert.deepStrictEqual(keys, rotatedKeys);
});
it('Should return the latest ative keys if no blockheight is specified', async function() {
const chainId = randomBytes(32).toString('hex');
const initialKeys = getRandomKeys();
const initialPublicKeys = initialKeys.map(k => k.public);
const newPublicIdKey = getRandomKeys(1)[0].public;
const newPublicIdKey2 = getRandomKeys(1)[0].public;
const replacement = generateIdentityKeyReplacementEntry(
chainId,
initialPublicKeys[1],
newPublicIdKey,
initialKeys[0]
);
const replacement2 = generateIdentityKeyReplacementEntry(
chainId,
initialPublicKeys[2],
newPublicIdKey2,
initialKeys[0]
);
const entries = [[getIdentityFirstEntry(initialPublicKeys), replacement, replacement2]];
const blockContexts = getBlockContexts([[1, 5, 15]]);
const cli = getMockedCli(chainId, entries, blockContexts);
const retriever = new IdentityInformationRetriever(cli);
const keys = await retriever.getActiveKeysAtHeight(chainId);
const rotatedKeys = [...initialPublicKeys];
rotatedKeys[1] = newPublicIdKey;
rotatedKeys[2] = newPublicIdKey2;
assert.deepStrictEqual(keys, rotatedKeys);
});
it('Should ignore garbage entry', async function() {
const chainId = randomBytes(32).toString('hex');
const initialKeys = getRandomKeys();
const initialPublicKeys = initialKeys.map(k => k.public);
const garbage = Entry.builder()
.extId('ReplaceKey', 'utf8')
.build();
const entries = [[getIdentityFirstEntry(initialPublicKeys), garbage]];
const blockContexts = getBlockContexts([[1, 5]]);
const cli = getMockedCli(chainId, entries, blockContexts);
const retriever = new IdentityInformationRetriever(cli);
const activeKeys = await retriever.getActiveKeysAtHeight(chainId, 99999);
assert.deepStrictEqual(activeKeys, initialPublicKeys);
});
it('Should throw if chain did not exist at height', async function() {
const chainId = randomBytes(32).toString('hex');
const initialPublicKeys = getRandomKeys().map(k => k.public);
const entries = [[getIdentityFirstEntry(initialPublicKeys)]];
const blockContexts = getBlockContexts([[100]]);
const cli = getMockedCli(chainId, entries, blockContexts);
try {
const retriever = new IdentityInformationRetriever(cli);
await retriever.getActiveKeysAtHeight(chainId, 10);
} catch (e) {
return assert.instanceOf(e, Error);
}
throw new Error('Should have thrown');
});
it('Should throw if chain is not an identity chain', async function() {
const chainId = randomBytes(32).toString('hex');
const entries = [
[
Entry.builder()
.content('random entry', 'utf8')
.build()
]
];
const blockContexts = getBlockContexts([[[1]]]);
const cli = getMockedCli(chainId, entries, blockContexts);
try {
const retriever = new IdentityInformationRetriever(cli);
await retriever.getActiveKeysAtHeight(chainId, 99999);
} catch (e) {
return assert.instanceOf(e, Error);
}
throw new Error('Should have thrown');
});
it('Should get keys after key rotation', async function() {
const chainId = randomBytes(32).toString('hex');
const initialKeys = getRandomKeys();
const initialPublicKeys = initialKeys.map(k => k.public);
const newPublicIdKey = getRandomKeys(1)[0].public;
const replacement = generateIdentityKeyReplacementEntry(
chainId,
initialPublicKeys[2],
newPublicIdKey,
initialKeys[1]
);
const entries = [[getIdentityFirstEntry(initialPublicKeys), replacement]];
const blockContexts = getBlockContexts([[1, 5]]);
const cli = getMockedCli(chainId, entries, blockContexts);
const retriever = new IdentityInformationRetriever(cli);
const activeKeys = await retriever.getActiveKeysAtHeight(chainId, 99999);
const rotatedKeys = [...initialPublicKeys];
rotatedKeys[2] = newPublicIdKey;
assert.deepStrictEqual(activeKeys, rotatedKeys);
});
it('Should get keys before key rotation', async function() {
const chainId = randomBytes(32).toString('hex');
const initialKeys = getRandomKeys();
const initialPublicKeys = initialKeys.map(k => k.public);
const newPublicIdKey = getRandomKeys(1)[0].public;
const replacement = generateIdentityKeyReplacementEntry(
chainId,
initialPublicKeys[2],
newPublicIdKey,
initialKeys[1]
);
const entries = [[getIdentityFirstEntry(initialPublicKeys), replacement]];
const blockContexts = getBlockContexts([[1, 5]]);
const cli = getMockedCli(chainId, entries, blockContexts);
const retriever = new IdentityInformationRetriever(cli);
const activeKeys = await retriever.getActiveKeysAtHeight(chainId, 3);
assert.deepStrictEqual(activeKeys, initialPublicKeys);
});
it('Should accept key rotation with the same priority', async function() {
const chainId = randomBytes(32).toString('hex');
const initialKeys = getRandomKeys();
const initialPublicKeys = initialKeys.map(k => k.public);
const newPublicIdKey = getRandomKeys(1)[0].public;
const replacement = generateIdentityKeyReplacementEntry(
chainId,
initialPublicKeys[2],
newPublicIdKey,
initialKeys[2]
);
const entries = [[getIdentityFirstEntry(initialPublicKeys), replacement]];
const blockContexts = getBlockContexts([[1, 5]]);
const cli = getMockedCli(chainId, entries, blockContexts);
const retriever = new IdentityInformationRetriever(cli);
const activeKeys = await retriever.getActiveKeysAtHeight(chainId, 99999);
const rotatedKeys = [...initialPublicKeys];
rotatedKeys[2] = newPublicIdKey;
assert.deepStrictEqual(activeKeys, rotatedKeys);
});
it('Should not take into account reuse of an existing key', async function() {
const chainId = randomBytes(32).toString('hex');
const initialKeys = getRandomKeys();
const initialPublicKeys = initialKeys.map(k => k.public);
const newPublicIdKey = getRandomKeys(1)[0].public;
const replacement1 = generateIdentityKeyReplacementEntry(
chainId,
initialPublicKeys[2],
newPublicIdKey,
initialKeys[0]
);
// Second rotation attempt to restore initialPublicKeys[2] in the active set, which is not allowed
const replacement2 = generateIdentityKeyReplacementEntry(
chainId,
newPublicIdKey,
initialPublicKeys[2],
initialKeys[0]
);
const entries = [[getIdentityFirstEntry(initialPublicKeys), replacement1, replacement2]];
const blockContexts = getBlockContexts([[1, 5, 12]]);
const cli = getMockedCli(chainId, entries, blockContexts);
const retriever = new IdentityInformationRetriever(cli);
const activeKeys = await retriever.getActiveKeysAtHeight(chainId, 99999);
const rotatedKeys = [...initialPublicKeys];
rotatedKeys[2] = newPublicIdKey;
assert.deepStrictEqual(activeKeys, rotatedKeys);
});
it('Should fail to rotate non active key', async function() {
const chainId = randomBytes(32).toString('hex');
const initialKeys = getRandomKeys();
const initialPublicKeys = initialKeys.map(k => k.public);
const newPublicIdKey = getRandomKeys(1)[0].public;
const newPublicIdKey2 = getRandomKeys(1)[0].public;
const replacement1 = generateIdentityKeyReplacementEntry(
chainId,
initialPublicKeys[2],
newPublicIdKey,
initialKeys[0]
);
// Second rotation attempt to rotate initialPublicKeys[2] key which is not in the active set anymore
const replacement2 = generateIdentityKeyReplacementEntry(
chainId,
initialPublicKeys[2],
newPublicIdKey2,
initialKeys[0]
);
const entries = [[getIdentityFirstEntry(initialPublicKeys), replacement1, replacement2]];
const blockContexts = getBlockContexts([[1, 5, 12]]);
const cli = getMockedCli(chainId, entries, blockContexts);
const retriever = new IdentityInformationRetriever(cli);
const activeKeys = await retriever.getActiveKeysAtHeight(chainId, 99999);
const rotatedKeys = [...initialPublicKeys];
rotatedKeys[2] = newPublicIdKey;
assert.deepStrictEqual(activeKeys, rotatedKeys);
});
it('Should not take into account key rotation by a lower priority key', async function() {
const chainId = randomBytes(32).toString('hex');
const initialKeys = getRandomKeys();
const initialPublicKeys = initialKeys.map(k => k.public);
const newPublicIdKey = getRandomKeys(1)[0].public;
const replacement = generateIdentityKeyReplacementEntry(
chainId,
initialPublicKeys[1],
newPublicIdKey,
initialKeys[2]
);
const entries = [[getIdentityFirstEntry(initialPublicKeys), replacement]];
const blockContexts = getBlockContexts([[1, 5]]);
const cli = getMockedCli(chainId, entries, blockContexts);
const retriever = new IdentityInformationRetriever(cli);
const activeKeys = await retriever.getActiveKeysAtHeight(chainId, 99999);
assert.deepStrictEqual(activeKeys, initialPublicKeys);
});
it('Should not take into account key rotation with a non valid key', async function() {
const chainId = randomBytes(32).toString('hex');
const initialKeys = getRandomKeys();
const initialPublicKeys = initialKeys.map(k => k.public);
const replacement = generateIdentityKeyReplacementEntry(
chainId,
initialPublicKeys[1],
'not an actual key',
initialKeys[0]
);
const entries = [[getIdentityFirstEntry(initialPublicKeys), replacement]];
const blockContexts = getBlockContexts([[1, 5]]);
const cli = getMockedCli(chainId, entries, blockContexts);
const retriever = new IdentityInformationRetriever(cli);
const activeKeys = await retriever.getActiveKeysAtHeight(chainId, 99999);
assert.deepStrictEqual(activeKeys, initialPublicKeys);
});
it('Should not take into account key rotation with invalid signature', async function() {
const chainId = randomBytes(32).toString('hex');
const initialKeys = getRandomKeys();
const initialPublicKeys = initialKeys.map(k => k.public);
const newPublicIdKey = getRandomKeys(1)[0].public;
const replacement = Entry.builder()
.chainId(chainId)
.extId('ReplaceKey', 'utf8')
.extId(initialPublicKeys[2], 'utf8')
.extId(newPublicIdKey, 'utf8')
.extId(randomBytes(64))
.extId(initialKeys[0].public, 'utf8')
.build();
const entries = [[getIdentityFirstEntry(initialPublicKeys), replacement]];
const blockContexts = getBlockContexts([[1, 5]]);
const cli = getMockedCli(chainId, entries, blockContexts);
const retriever = new IdentityInformationRetriever(cli);
const activeKeys = await retriever.getActiveKeysAtHeight(chainId, 99999);
assert.deepStrictEqual(activeKeys, initialPublicKeys);
});
it('Should get cached keys', async function() {
const chainId = randomBytes(32).toString('hex');
const initialKeys = getRandomKeys();
const initialPublicKeys = initialKeys.map(k => k.public);
const newPublicIdKey = getRandomKeys(1)[0].public;
const newPublicIdKey2 = getRandomKeys(1)[0].public;
const replacement = generateIdentityKeyReplacementEntry(
chainId,
initialPublicKeys[2],
newPublicIdKey,
initialKeys[0]
);
const replacement2 = generateIdentityKeyReplacementEntry(
chainId,
initialPublicKeys[1],
newPublicIdKey2,
initialKeys[0]
);
const entries = [[getIdentityFirstEntry(initialPublicKeys), replacement, replacement2]];
const blockContexts = getBlockContexts([[1, 5, 10]]);
const cli = getMockedCli(chainId, entries, blockContexts);
const retriever = new IdentityInformationRetriever(cli);
await retriever.getActiveKeysAtHeight(chainId, 99999);
// Now get keys from the cache
const set1 = await retriever.getActiveKeysAtHeight(chainId, 1);
const set2 = await retriever.getActiveKeysAtHeight(chainId, 3);
const set3 = await retriever.getActiveKeysAtHeight(chainId, 5);
const set4 = await retriever.getActiveKeysAtHeight(chainId, 7);
const set5 = await retriever.getActiveKeysAtHeight(chainId, 10);
assert.deepStrictEqual(set1, initialPublicKeys);
assert.deepStrictEqual(set2, initialPublicKeys);
const rotatedKeys = [...initialPublicKeys];
rotatedKeys[2] = newPublicIdKey;
assert.deepStrictEqual(set4, rotatedKeys);
assert.deepStrictEqual(set3, rotatedKeys);
const rotatedKeys2 = [...rotatedKeys];
rotatedKeys2[1] = newPublicIdKey2;
assert.deepStrictEqual(set5, rotatedKeys2);
});
it('Should get fetch keys missing from cache', async function() {
const chainId = randomBytes(32).toString('hex');
const initialKeys = getRandomKeys();
const initialPublicKeys = initialKeys.map(k => k.public);
const newPublicIdKey = getRandomKeys(1)[0].public;
const newPublicIdKey2 = getRandomKeys(1)[0].public;
const replacement = generateIdentityKeyReplacementEntry(
chainId,
initialPublicKeys[2],
newPublicIdKey,
initialKeys[0]
);
const replacement2 = generateIdentityKeyReplacementEntry(
chainId,
initialPublicKeys[1],
newPublicIdKey2,
initialKeys[0]
);
const entries = [
[getIdentityFirstEntry(initialPublicKeys), replacement],
[replacement, replacement2]
];
const blockContexts = getBlockContexts([
[1, 5],
[5, 10]
]);
const cli = getMockedCli(chainId, entries, blockContexts);
const retriever = new IdentityInformationRetriever(cli);
await retriever.getActiveKeysAtHeight(chainId, 8);
// Now get keys from the cache + fetch the last entry missing (replacement2)
const set = await retriever.getActiveKeysAtHeight(chainId, 11);
const rotatedKeys = [...initialPublicKeys];
rotatedKeys[2] = newPublicIdKey;
rotatedKeys[1] = newPublicIdKey2;
assert.deepStrictEqual(set, rotatedKeys);
});
it('Should get identity name', async function() {
const chainId = randomBytes(32).toString('hex');
const name = [randomBytes(4), randomBytes(12), randomBytes(20)];
const firstEntry = generateIdentityChain(
name,
getRandomKeys().map(k => k.public)
).firstEntry;
const cli = new FactomCli();
const mock = sinon.mock(cli);
mock.expects('getFirstEntry')
.once()
.withArgs(chainId)
.returns(firstEntry);
const retriever = new IdentityInformationRetriever(cli);
const readName = await retriever.getIdentityName(chainId);
// Calling again should fetch name from the cache and not call getFirstEntry
await retriever.getIdentityName(chainId);
assert.deepStrictEqual(readName, name);
});
it('Should cache identity name during call to getActiveKeysAtHeight', async function() {
const chainId = randomBytes(32).toString('hex');
const initialPublicKeys = getRandomKeys().map(k => k.public);
const entries = [[getIdentityFirstEntry(initialPublicKeys)]];
const blockContexts = getBlockContexts([[1]]);
const cli = getMockedCli(chainId, entries, blockContexts);
const retriever = new IdentityInformationRetriever(cli);
await retriever.getActiveKeysAtHeight(chainId, 99999);
const readName = await retriever.getIdentityName(chainId);
assert.deepStrictEqual(readName, [Buffer.from('name', 'utf8')]);
});
it('Should initialize the cache', async function() {
const chainId = randomBytes(32).toString('hex');
const publicKeys = getRandomKeys().map(k => k.public);
const cache = { keys: {}, names: {} };
cache.keys[chainId] = [
{
height: 8,
activeKeys: publicKeys,
allKeys: publicKeys
}
];
cache.names[chainId] = [Buffer.from('from'), Buffer.from('cache')];
const retriever = new IdentityInformationRetriever(undefined, { initialCacheData: cache });
const activeKeys = await retriever.getActiveKeysAtHeight(chainId, 8);
const readName = await retriever.getIdentityName(chainId);
assert.deepStrictEqual(activeKeys, publicKeys);
assert.deepStrictEqual(readName, [Buffer.from('from'), Buffer.from('cache')]);
});
it('Should not save the cache if no new key rotation happened', async function() {
const chainId = randomBytes(32).toString('hex');
const initialPublicKeys = getRandomKeys().map(k => k.public);
const entries = [
[getIdentityFirstEntry(initialPublicKeys)],
[getIdentityFirstEntry(initialPublicKeys)]
];
const blockContexts = getBlockContexts([[11], [11]]);
const cli = getMockedCli(chainId, entries, blockContexts);
const save = sinon.spy();
const retriever = new IdentityInformationRetriever(cli, { save });
await retriever.getActiveKeysAtHeight(chainId);
await retriever.getActiveKeysAtHeight(chainId);
assert.isTrue(save.calledOnce);
});
it('Should save the cache', async function() {
const chainId = randomBytes(32).toString('hex');
const initialPublicKeys = getRandomKeys().map(k => k.public);
const entries = [[getIdentityFirstEntry(initialPublicKeys)]];
const blockContexts = getBlockContexts([[11]]);
const cli = getMockedCli(chainId, entries, blockContexts);
const save = sinon.spy();
const retriever = new IdentityInformationRetriever(cli, { save });
await retriever.getActiveKeysAtHeight(chainId, 99999);
assert.isTrue(save.calledOnce);
const expectedCacheValue = { keys: {}, names: {} };
expectedCacheValue.keys[chainId] = [
{
height: 11,
activeKeys: initialPublicKeys,
allKeys: initialPublicKeys
}
];
expectedCacheValue.names[chainId] = [Buffer.from('name').toString('hex')];
assert.deepStrictEqual(save.firstCall.args[0], expectedCacheValue);
});
it('Should use saved data as cache initialization', async function() {
const chainId = randomBytes(32).toString('hex');
const initialPublicKeys = getRandomKeys().map(k => k.public);
const entries = [[getIdentityFirstEntry(initialPublicKeys)]];
const blockContexts = getBlockContexts([[11]]);
const cli = getMockedCli(chainId, entries, blockContexts);
const save = sinon.spy();
const retriever = new IdentityInformationRetriever(cli, { save });
await retriever.getActiveKeysAtHeight(chainId, 99999);
const persisted = save.firstCall.args[0];
const save2 = sinon.spy();
const retriever2 = new IdentityInformationRetriever(undefined, {
save2,
initialCacheData: persisted
});
const activeKeys = await retriever2.getActiveKeysAtHeight(chainId, 11);
const name = await retriever2.getIdentityName(chainId);
assert.isTrue(save2.notCalled);
assert.deepStrictEqual(activeKeys, initialPublicKeys);
assert.deepStrictEqual(name, [Buffer.from('name')]);
});
});
/********************
* Helper functions
********************/
function getBlockContexts(heights) {
return heights.map(a => a.map(h => ({ directoryBlockHeight: h })));
}
function getRandomKeys(n = 3) {
return Array(n)
.fill('')
.map(() => generateRandomIdentityKeyPair());
}
function getIdentityFirstEntry(publicKeys) {
return generateIdentityChain(['name'], publicKeys).firstEntry;
}
function getMockedCli(chainId, entries, blockContexts) {
assert.strictEqual(entries.length, blockContexts.length);
entries.forEach((e, i) => e.length === blockContexts[i].length);
const cli = new FactomCli();
const mock = sinon.mock(cli);
const stub = mock
.expects('rewindChainWhile')
.exactly(entries.length)
.withArgs(chainId);
for (let j = 0; j < entries.length; ++j) {
stub.onCall(j).callsFake(async function(chainId, condition, f) {
for (let i = entries[j].length - 1; i >= 0; --i) {
const b = await condition(
Entry.builder(entries[j][i])
.blockContext(blockContexts[j][i])
.build()
);
if (b) {
await f(
Entry.builder(entries[j][i])
.blockContext(blockContexts[j][i])
.build()
);
}
}
});
}
return cli;
}