From 0899ac5818b47501c0db54955732bebd3cc563ab Mon Sep 17 00:00:00 2001 From: Andre Staltz Date: Mon, 10 Jul 2023 15:52:18 +0300 Subject: [PATCH] add identity.consent() API --- lib/index.js | 67 +++++++++++++++++++++++++++++--------- lib/msg-v3/constants.js | 7 ++++ lib/msg-v3/index.js | 19 ++++++----- lib/msg-v3/validation.js | 8 +++-- test/identity-add.test.js | 6 ++++ test/msg-v3/create.test.js | 4 +-- 6 files changed, 84 insertions(+), 27 deletions(-) create mode 100644 lib/msg-v3/constants.js diff --git a/lib/index.js b/lib/index.js index 766e09b..b046d24 100644 --- a/lib/index.js +++ b/lib/index.js @@ -8,7 +8,9 @@ const b4a = require('b4a') const base58 = require('bs58') // @ts-ignore const Obz = require('obz') +const Keypair = require('ppppp-keypair') const MsgV3 = require('./msg-v3') +const { SIGNATURE_TAG_IDENTITY_ADD, IDENTITY_SELF } = require('./msg-v3/constants') const { ReadyGate } = require('./utils') const { decrypt } = require('./encryption') @@ -272,7 +274,7 @@ function initDB(peer, config) { const tangle = new DBTangle(tangleRootHash, records()) const pubkeys = new Set() - if (msg.metadata.identity && msg.metadata.identity !== 'self') { + if (msg.metadata.identity && msg.metadata.identity !== IDENTITY_SELF) { const identityTangle = new DBTangle(msg.metadata.identity, records()) if (!identityTangle.has(msg.metadata.identity)) { // prettier-ignore @@ -323,7 +325,7 @@ function initDB(peer, config) { */ function getIdentityId(rec) { if (!rec.msg) return null - if (rec.msg.metadata.identity === 'self') { + if (rec.msg.metadata.identity === IDENTITY_SELF) { for (const tangleId in rec.msg.metadata.tangles) { return tangleId } @@ -343,8 +345,8 @@ function initDB(peer, config) { * @param {CB} cb */ function findIdentity(opts, cb) { - if (!opts.domain) - return cb(new Error('identity.find() requires a `domain`')) + // prettier-ignore + if (!opts.domain) return cb(new Error('identity.find() requires a `domain`')) const keypair = opts?.keypair ?? config.keypair const domain = opts.domain @@ -354,7 +356,7 @@ function initDB(peer, config) { if (!rec.msg) continue if (!rec.msg.data) continue if ( - rec.msg.metadata.identity === 'self' && + rec.msg.metadata.identity === IDENTITY_SELF && rec.msg.data.add === keypair.public && rec.msg.metadata.domain === domain ) { @@ -365,7 +367,7 @@ function initDB(peer, config) { // prettier-ignore cb(new Error(`identity.find() failed to find ID in ${JSON.stringify(rec.msg)}`)) } - break + return } } // prettier-ignore @@ -382,8 +384,8 @@ function initDB(peer, config) { * @param {CB} cb */ function createIdentity(opts, cb) { - if (!opts.domain) - return cb(new Error('identity.create() requires a `domain`')) + // prettier-ignore + if (!opts.domain) return cb(new Error('identity.create() requires a `domain`')) const keypair = opts?.keypair ?? config.keypair const domain = opts.domain @@ -425,25 +427,59 @@ function initDB(peer, config) { } /** - * @param {{ keypair: Keypair; identity: string; }} opts + * @param {{ + * keypair?: Keypair; + * identity: string; + * }} opts + * @returns {string} + */ + function consentToIdentity(opts) { + // prettier-ignore + if (!opts.identity) throw new Error('identity.consent() requires an `identity`') + const keypair = opts?.keypair ?? config.keypair + + const signableBuf = b4a.from( + SIGNATURE_TAG_IDENTITY_ADD + base58.decode(opts.identity), + 'utf8' + ) + return Keypair.sign(keypair, signableBuf) + } + + /** + * @param {{ + * keypair: Keypair; + * identity: string; + * consent: string + * }} opts * @param {CB} cb */ function addToIdentity(opts, cb) { - if (!opts?.keypair) - return cb(new Error('identity.add() requires a `keypair`')) - if (!opts?.identity) - return cb(new Error('identity.add() requires a `identity`')) + // prettier-ignore + if (!opts?.keypair) return cb(new Error('identity.add() requires a `keypair`')) + // prettier-ignore + if (!opts?.identity) return cb(new Error('identity.add() requires a `identity`')) + // prettier-ignore + if (!opts?.consent) return cb(new Error('identity.add() requires a `consent`')) const addedKeypair = opts.keypair const signingKeypair = config.keypair + // Verify consent: + const signableBuf = b4a.from( + SIGNATURE_TAG_IDENTITY_ADD + base58.decode(opts.identity), + ) + if (!Keypair.verify(addedKeypair, signableBuf, opts.consent)) { + // prettier-ignore + return cb(new Error('identity.add() failed because the consent is invalid')) + } + // Fill-in tangle opts: const tangles = populateTangles([opts.identity]) const fullOpts = { - identity: 'self', + identity: IDENTITY_SELF, identityTips: null, tangles, keypair: signingKeypair, - data: { add: addedKeypair.public }, + data: { add: addedKeypair.public, consent: opts.consent }, domain: 'identity', } @@ -652,6 +688,7 @@ function initDB(peer, config) { create: createIdentity, findOrCreate: findOrCreateIdentity, add: addToIdentity, + consent: consentToIdentity, }, feed: { publish: publishToFeed, diff --git a/lib/msg-v3/constants.js b/lib/msg-v3/constants.js new file mode 100644 index 0000000..078fbfa --- /dev/null +++ b/lib/msg-v3/constants.js @@ -0,0 +1,7 @@ +module.exports = { + /** @type {'self'} */ + IDENTITY_SELF: 'self', + + SIGNATURE_TAG_MSG_V3: ':msg-v3:', + SIGNATURE_TAG_IDENTITY_ADD: ':identity-add:', +} diff --git a/lib/msg-v3/index.js b/lib/msg-v3/index.js index 64c61c0..ba849e7 100644 --- a/lib/msg-v3/index.js +++ b/lib/msg-v3/index.js @@ -17,6 +17,7 @@ const { validateMsgHash, } = require('./validation') const Tangle = require('./tangle') +const { IDENTITY_SELF, SIGNATURE_TAG_MSG_V3 } = require('./constants') /** * @typedef {import('ppppp-keypair').Keypair} Keypair @@ -57,8 +58,6 @@ const Tangle = require('./tangle') * }} CreateOpts */ -const IDENTITY_SELF = /** @type {const} */ 'self' - /** * @param {string} id * @param {string} domain @@ -138,9 +137,11 @@ function create(opts) { } if ((err = validateData(msg))) throw err - // TODO: add a label prefix to the metadata before signing - const metadataBuf = b4a.from(stringify(msg.metadata), 'utf8') - msg.sig = Keypair.sign(opts.keypair, metadataBuf) + const signableBuf = b4a.from( + SIGNATURE_TAG_MSG_V3 + stringify(msg.metadata), + 'utf8' + ) + msg.sig = Keypair.sign(opts.keypair, signableBuf) return msg } @@ -171,9 +172,11 @@ function createRoot(id, domain, keypair) { sig: '', } - // TODO: add a label prefix to the metadata before signing - const metadataBuf = b4a.from(stringify(msg.metadata), 'utf8') - msg.sig = Keypair.sign(keypair, metadataBuf) + const signableBuf = b4a.from( + SIGNATURE_TAG_MSG_V3 + stringify(msg.metadata), + 'utf8' + ) + msg.sig = Keypair.sign(keypair, signableBuf) return msg } diff --git a/lib/msg-v3/validation.js b/lib/msg-v3/validation.js index 407afe5..bb688d8 100644 --- a/lib/msg-v3/validation.js +++ b/lib/msg-v3/validation.js @@ -6,6 +6,7 @@ const stringify = require('json-canon') const Tangle = require('./tangle') const representData = require('./represent-data') const isFeedRoot = require('./is-feed-root') +const { SIGNATURE_TAG_MSG_V3, IDENTITY_SELF } = require('./constants') /** * @typedef {import('.').Msg} Msg @@ -90,7 +91,7 @@ function validateIdentityPubkey(msg, pubkeys) { if ( msg.metadata.identity && - msg.metadata.identity !== 'self' && + msg.metadata.identity !== IDENTITY_SELF && !pubkeys.has(msg.pubkey) ) { // prettier-ignore @@ -150,7 +151,10 @@ function validateSignature(msg) { return `invalid message: sig "${sig}" should have been a base58 string\n` + JSON.stringify(msg) } - const signableBuf = b4a.from(stringify(msg.metadata), 'utf8') + const signableBuf = b4a.from( + SIGNATURE_TAG_MSG_V3 + stringify(msg.metadata), + 'utf8' + ) const keypair = { curve: 'ed25519', public: msg.pubkey } const verified = Keypair.verify(keypair, signableBuf, sig) if (!verified) { diff --git a/test/identity-add.test.js b/test/identity-add.test.js index c548770..0d3aa5f 100644 --- a/test/identity-add.test.js +++ b/test/identity-add.test.js @@ -26,9 +26,12 @@ test('identity.add()', async (t) => { domain: 'person', }) + const consent = peer.db.identity.consent({ identity: id, keypair: keypair2 }) + const identityRec1 = await p(peer.db.identity.add)({ identity: id, keypair: keypair2, + consent, }) assert.ok(identityRec1, 'identityRec1 exists') const { hash, msg } = identityRec1 @@ -64,9 +67,12 @@ test('publish with a key in the identity', async (t) => { domain: 'person', }) const identityMsg0 = peer.db.get(identity) + + const consent = peer.db.identity.consent({ identity, keypair: keypair2 }) const identityRec1 = await p(peer.db.identity.add)({ identity, keypair: keypair2, + consent, }) const postRec = await p(peer.db.feed.publish)({ diff --git a/test/msg-v3/create.test.js b/test/msg-v3/create.test.js index 9cbef7b..00241bd 100644 --- a/test/msg-v3/create.test.js +++ b/test/msg-v3/create.test.js @@ -95,7 +95,7 @@ test('MsgV3.create()', (t) => { ) assert.equal( msg1.sig, - '2FhnKsDKCxEV4JUM1nmPp3oSFJ8zL7r4SjMNogDHeAzCWQLgVmKiexgUDSE4k9C3eT4Uy3SZbBhRY75WJAqvtHHf', + '3ucLkFxXJkbX6N7qZQm5PNop2tQ5Z1E9oCVB4HCZjeD3Mn7EXMrgZzCDZfpLTVUUBRqSBQJFxL1j5jNWKFeidHgV', 'sig' ) @@ -155,7 +155,7 @@ test('MsgV3.create()', (t) => { ) assert.equal( msg2.sig, - '3B6WQvDdKvRhZeZDfE9LY4HrnhZTJHRJ86FazBg1xxco2S1eHG44UwR9TSpthiXQ1X51h2VeDeGPV6Fdma69BMN9', + 'RtHPPccZNp6c65SnCrfjsNVB6We6G4Ja1oi68AdLuzxSjWNayxepagYJQwgP635E4b55xNGckMiFvJF9Vsn3oAi', 'sig' )