From e40c7cff09196cd60307ba5d01701cf41e82a395 Mon Sep 17 00:00:00 2001 From: Powersource Date: Mon, 25 Mar 2024 16:46:28 +0100 Subject: [PATCH] Prune the accountTangle in getSigkeysInAccount (#23) --- lib/index.js | 26 +++++++---- lib/msg-v4/index.js | 2 +- test/msg-v4/tangles.test.js | 4 +- test/sigkeys.test.js | 88 +++++++++++++++++++++++++++++++++++++ 4 files changed, 108 insertions(+), 12 deletions(-) create mode 100644 test/sigkeys.test.js diff --git a/lib/index.js b/lib/index.js index 7d4387e..d8c7be9 100644 --- a/lib/index.js +++ b/lib/index.js @@ -376,11 +376,11 @@ function initDB(peer, config) { /** * @param {Pick} rec - * @returns {Tangle | null} + * @returns {DBTangle | null} */ function getAccountTangle(rec) { const accountID = getAccountID(rec) - let accountTangle = /** @type {Tangle | null} */ (null) + let accountTangle = /** @type {DBTangle | null} */ (null) if (accountID) { accountTangle = new DBTangle(accountID, records(), get) if (rec.id === accountID) { @@ -397,15 +397,20 @@ function initDB(peer, config) { * Find which sigkeys are authorized to sign this msg given the account. * * @private - * @param {Tangle | null} accountTangle + * @param {DBTangle | null} accountTangle + * @param {Msg['metadata']['accountTips']} accountTipsInMsg * @returns {Set} */ - function getSigkeysInAccount(accountTangle) { + function getSigkeysInAccount(accountTangle, accountTipsInMsg) { const sigkeys = new Set() if (!accountTangle) return sigkeys - // TODO: prune the accountTangle beyond msg.metadata.accountTips - for (const msgID of accountTangle.topoSort()) { - const msg = get(msgID) + + const prunedTangle = accountTangle.slice( + undefined, + accountTipsInMsg ?? undefined + ) + + for (const msg of prunedTangle) { if (!msg?.data) continue /** @type {AccountData} */ const data = msg.data @@ -461,14 +466,17 @@ function initDB(peer, config) { } // Identify the account and its sigkeys: - /** @type {Tangle | null} */ + /** @type {DBTangle | null} */ let accountTangle try { accountTangle = getAccountTangle(rec) } catch (err) { return new Error('Unknown account tangle owning this msg', { cause: err }) } - const sigkeys = getSigkeysInAccount(accountTangle) + const sigkeys = getSigkeysInAccount( + accountTangle, + rec.msg.metadata.accountTips + ) // Don't accept ghosts to come back, unless they are trail msgs if (!!rec.msg.data && ghosts.read(tangleID).has(rec.id)) { diff --git a/lib/msg-v4/index.js b/lib/msg-v4/index.js index b816973..cb4044e 100644 --- a/lib/msg-v4/index.js +++ b/lib/msg-v4/index.js @@ -306,7 +306,7 @@ function isRoot(msg) { * @returns {msg is FeedMsg} */ function isFeedMsg(msg) { - const {account, accountTips} = msg.metadata + const { account, accountTips } = msg.metadata return Array.isArray(accountTips) && account !== 'self' && account !== 'any' } diff --git a/test/msg-v4/tangles.test.js b/test/msg-v4/tangles.test.js index 1697655..71417f0 100644 --- a/test/msg-v4/tangles.test.js +++ b/test/msg-v4/tangles.test.js @@ -235,6 +235,6 @@ test('MsgV4.Tangle can add msgs in random order', (t) => { tangle.add(msgID2, msg2) tangle.add(msgID1, msg1) - assert.deepEqual(tangle.topoSort(), [mootAID, msgID1, msgID2]); + assert.deepEqual(tangle.topoSort(), [mootAID, msgID1, msgID2]) assert.deepEqual([...tangle.tips], [msgID2], 'tangle tips') -}) \ No newline at end of file +}) diff --git a/test/sigkeys.test.js b/test/sigkeys.test.js new file mode 100644 index 0000000..4322036 --- /dev/null +++ b/test/sigkeys.test.js @@ -0,0 +1,88 @@ +const test = require('node:test') +const assert = require('node:assert') +const path = require('node:path') +const p = require('node:util').promisify +const os = require('node:os') +const rimraf = require('rimraf') +const Keypair = require('ppppp-keypair') +const { createPeer } = require('./util') +const MsgV4 = require('../lib/msg-v4') + +const DIR = path.join(os.tmpdir(), 'ppppp-db-sigkeys') +const DIR2 = path.join(os.tmpdir(), 'ppppp-db-sigkeys2') +rimraf.sync(DIR) +rimraf.sync(DIR2) + +test('sigkeys', async (t) => { + await t.test( + "Can't add msg that is signed by key newer than what accountTips points to", + async () => { + const keypair1 = Keypair.generate('ed25519', 'alice') + const keypair2 = Keypair.generate('ed25519', 'alice2') + const keypairOther = Keypair.generate('ed25519', 'bob') + + const peer = createPeer({ keypair: keypair1, path: DIR }) + const peerOther = createPeer({ keypair: keypairOther, path: DIR2 }) + + await peer.db.loaded() + await peerOther.db.loaded() + + const account = await p(peer.db.account.create)({ + keypair: keypair1, + subdomain: 'person', + }) + const accountMsg0 = peer.db.get(account) + + const consent = peer.db.account.consent({ account, keypair: keypair2 }) + + const accountRec1 = await p(peer.db.account.add)({ + account, + keypair: keypair2, + consent, + powers: ['external-encryption'], + }) + + const goodRec = await p(peer.db.feed.publish)({ + account, + domain: 'post', + data: { text: 'potatoGood' }, + keypair: keypair2, + }) + + const postMootId = peer.db.feed.getID(account, 'post') + const postMootMsg = peer.db.get(postMootId) + + const tangle = new MsgV4.Tangle(postMootId) + tangle.add(postMootId, postMootMsg) + tangle.add(goodRec.id, goodRec.msg) + const badMsg = MsgV4.create({ + account, + accountTips: [account], // intentionally excluding keypair2 + domain: 'post', + keypair: keypair2, // intentionally using newer key than accountTips points to + tangles: { + [postMootId]: tangle, + }, + data: { text: 'potato' }, + }) + await assert.rejects( + p(peer.db.add)(badMsg, postMootId), + /add\(\) failed to verify msg/, + "Shouldn't be able to add() own bad msg" + ) + + await p(peerOther.db.add)(accountMsg0, account) + await p(peerOther.db.add)(accountRec1.msg, account) + await p(peerOther.db.add)(postMootMsg, postMootId) + await p(peerOther.db.add)(goodRec.msg, postMootId) + await assert.rejects( + p(peerOther.db.add)(badMsg, postMootId), + /add\(\) failed to verify msg/, + "Shouldn't be able to add() someone else's bad msg" + ) + + await p(peer.close)() + await p(peerOther.close)() + } + ) +})