From 79bc497911a943b90b48cf9dc9a73c09992a5364 Mon Sep 17 00:00:00 2001 From: Andre Staltz Date: Fri, 14 Jul 2023 16:14:00 +0300 Subject: [PATCH] detailed tag union for adding identity keys --- lib/index.js | 37 +++++++++++----- lib/msg-v3/index.js | 52 ++++++++++++++++++++++- package.json | 2 +- protospec.md | 82 ++++++++++++++++++++++++++++++++---- test/identity-add.test.js | 16 ++++++- test/identity-create.test.js | 30 ++++++++++--- test/msg-v3/create.test.js | 76 ++++++++++++++++++++++++++------- 7 files changed, 251 insertions(+), 44 deletions(-) diff --git a/lib/index.js b/lib/index.js index 4b93020..2d04e61 100644 --- a/lib/index.js +++ b/lib/index.js @@ -22,6 +22,7 @@ const { decrypt } = require('./encryption') * @typedef {import('ppppp-keypair').KeypairPublicSlice} KeypairPublicSlice * @typedef {import('ppppp-keypair').KeypairPrivateSlice} KeypairPrivateSlice * @typedef {import('./msg-v3').Msg} Msg + * @typedef {import('./msg-v3').IdentityData} IdentityData * @typedef {import('./encryption').EncryptionFormat} EncryptionFormat * * @typedef {Buffer | Uint8Array} B4A @@ -287,8 +288,13 @@ function initDB(peer, config) { } for (const msgHash of identityTangle.topoSort()) { const msg = get(msgHash) - if (!msg?.data?.add) continue - pubkeys.add(msg.data.add) + if (!msg?.data) continue + /** @type {IdentityData} */ + const data = msg.data + if (data.action !== 'add') continue + if (data.add.key.purpose !== 'sig') continue + if (data.add.key.algorithm !== 'ed25519') continue + pubkeys.add(data.add.key.bytes) } } @@ -360,11 +366,10 @@ function initDB(peer, config) { if (!rec) continue if (!rec.msg) continue if (!rec.msg.data) continue - if ( - rec.msg.metadata.identity === IDENTITY_SELF && - rec.msg.data.add === keypair.public && - rec.msg.metadata.domain === domain - ) { + if (rec.msg.metadata.identity !== IDENTITY_SELF) continue + if (rec.msg.metadata.domain !== domain) continue + const data = /** @type {IdentityData} */ (rec.msg.data) + if (data.action === 'add' && data.add.key.bytes === keypair.public) { const identityId = getIdentityId(rec) if (identityId) { cb(null, identityId) @@ -489,14 +494,26 @@ function initDB(peer, config) { return cb(new Error('identity.add() failed because the consent is invalid')) } + /** @type {IdentityData} */ + const data = { + action: 'add', + add: { + key: { + purpose: 'sig', + algorithm: 'ed25519', + bytes: addedKeypair.public, + }, + consent, + }, + } + // Fill-in tangle opts: - const tangles = populateTangles([opts.identity]) const fullOpts = { identity: IDENTITY_SELF, identityTips: null, - tangles, + tangles: populateTangles([opts.identity]), keypair: signingKeypair, - data: { add: addedKeypair.public, consent: opts.consent }, + data, domain: 'identity', } diff --git a/lib/msg-v3/index.js b/lib/msg-v3/index.js index ba849e7..bcc7d4d 100644 --- a/lib/msg-v3/index.js +++ b/lib/msg-v3/index.js @@ -49,6 +49,42 @@ const { IDENTITY_SELF, SIGNATURE_TAG_MSG_V3 } = require('./constants') * }} Msg * * @typedef {{ + * action: 'add', add: IdentityAdd + * } | { + * action: 'del', del: IdentityDel + * }} IdentityData + * + * @typedef {{ + * key: IdentityKey; + * nonce?: string; + * consent?: string; + * }} IdentityAdd + * + * @typedef {{ + * key: IdentityKey; + * }} IdentityDel + * + * @typedef {{ + * purpose: 'sig'; + * algorithm: 'ed25519'; + * bytes: string; + * }} SigKey + * + * @typedef {{ + * purpose: 'subidentity'; + * algorithm: 'tangle'; + * bytes: string; + * }} SubidentityKey; + * + * @typedef {{ + * purpose: 'box'; + * algorithm: 'x25519-xsalsa20-poly1305'; + * bytes: string; + * }} BoxKey; + * + * @typedef {SigKey | SubidentityKey | BoxKey} IdentityKey + * + * @typedef {{ * data: any; * domain: string; * keypair: Keypair; @@ -192,9 +228,21 @@ function getRandomNonce() { * @returns {Msg} */ function createIdentity(keypair, domain, nonce = getRandomNonce) { - const actualNonce = typeof nonce === 'function' ? nonce() : nonce + /** @type {IdentityData} */ + const data = { + action: 'add', + add: { + key: { + purpose: 'sig', + algorithm: 'ed25519', + bytes: keypair.public, + }, + nonce: typeof nonce === 'function' ? nonce() : nonce, + }, + } + return create({ - data: { add: keypair.public, nonce: actualNonce }, + data, identity: IDENTITY_SELF, identityTips: null, keypair, diff --git a/package.json b/package.json index 2b001f4..8ee448e 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "bs58": "~5.0.0", "json-canon": "~1.0.0", "obz": "~1.1.0", - "ppppp-keypair": "github:staltz/ppppp-keypair", + "ppppp-keypair": "file:../keypair", "promisify-4loc": "~1.0.0", "push-stream": "~11.2.0", "set.prototype.union": "~1.0.2" diff --git a/protospec.md b/protospec.md index 29391f7..9a767f1 100644 --- a/protospec.md +++ b/protospec.md @@ -47,10 +47,7 @@ Msgs in an identity tangle are special because they have empty `identity` and `i ```typescript interface Msg { - data: { - add: string // pubkey being added to the identity - nonce?: string // nonce required only on the identity tangle's root - } + data: IdentityData metadata: { dataHash: ContentHash dataSize: number @@ -68,13 +65,82 @@ interface Msg { pubkey: Pubkey sig: Signature } + +type IdentityData = + | { action: 'add' add: IdentityAdd } + | { action: 'del' del: IdentityDel } + +type IdentityAdd = { + key: Key + nonce?: string // nonce required only on the identity tangle's root + consent?: string // base58 encoded signature of the string `:identity-add:` where `` is the identity's ID, required only on non-root msgs +} + +type IdentityDel = { + key: Key +} + +type Key = + | { + purpose: 'sig' // digital signatures + algorithm: 'ed25519' // libsodium crypto_sign_detached + bytes: string // base58 encoded string for the public key being added + } + | { + purpose: 'subidentity' + algorithm: 'tangle' // PPPPP tangle + bytes: string // subidentity ID + } + | { + // WIP!! + purpose: 'box' // asymmetric encryption + algorithm: 'x25519-xsalsa20-poly1305' // libsodium crypto_box_easy + bytes: string // base58 encoded string of the public key + } ``` -The `data` object varies: +Examples of `IdentityData`: -- In the first message: `{ add: , nonce: }` -- In subsequent messages: `{ add: , consent: }` - - Where `` is the base58-encoded signature of the string `:identity-add:` where `` is the identity's ID +- Registering the first signing pubkey: + ```json + { + "action": "add", + "add": { + "key": { + "purpose": "sig", + "algorithm": "ed25519", + "bytes": "3JrJiHEQzRFMzEqWawfBgq2DSZDyihP1NHXshqcL8pB9" + }, + "nonce": "6GHR1ZFFSB3C5qAGwmSwVH8f7byNo8Cqwn5PcyG3qDvS" + } + } + ``` +- Registering a subidentity: + ```json + { + "action": "add", + "add": { + "key": { + "purpose": "subidentity", + "algorithm": "tangle", + "bytes": "6yqq7iwyJEKdofJ3xpRLEq" + } + } + } + ``` +- Revoking a signing pubkey: + ```json + { + "action": "del", + "del": { + "key": { + "purpose": "sig", + "algorithm": "ed25519", + "bytes": "3JrJiHEQzRFMzEqWawfBgq2DSZDyihP1NHXshqcL8pB9" + } + } + } + ``` ## Feed root diff --git a/test/identity-add.test.js b/test/identity-add.test.js index 74f9cba..4903bb0 100644 --- a/test/identity-add.test.js +++ b/test/identity-add.test.js @@ -36,7 +36,21 @@ test('identity.add()', async (t) => { assert.ok(identityRec1, 'identityRec1 exists') const { hash, msg } = identityRec1 assert.ok(hash, 'hash exists') - assert.equal(msg.data.add, keypair2.public, 'msg.data.add NEW KEY') + assert.deepEqual( + msg.data, + { + action: 'add', + add: { + key: { + purpose: 'sig', + algorithm: 'ed25519', + bytes: keypair2.public, + }, + consent, + }, + }, + 'msg.data.add NEW KEY' + ) assert.equal(msg.metadata.identity, 'self', 'msg.metadata.identity') assert.equal(msg.metadata.identityTips, null, 'msg.metadata.identityTips') assert.deepEqual( diff --git a/test/identity-create.test.js b/test/identity-create.test.js index fc8cff0..a8bcd69 100644 --- a/test/identity-create.test.js +++ b/test/identity-create.test.js @@ -19,10 +19,27 @@ test('identity.create() with just "domain"', async (t) => { .call(null, { keypair, path: DIR }) await peer.db.loaded() - const identity = await p(peer.db.identity.create)({ domain: 'person' }) + const identity = await p(peer.db.identity.create)({ + domain: 'person', + _nonce: 'MYNONCE', + }) assert.ok(identity, 'identityRec0 exists') const msg = peer.db.get(identity) - assert.equal(msg.data.add, keypair.public, 'msg.data.add') + assert.deepEqual( + msg.data, + { + action: 'add', + add: { + key: { + purpose: 'sig', + algorithm: 'ed25519', + bytes: keypair.public, + }, + nonce: 'MYNONCE', + }, + }, + 'msg.data.add' + ) assert.equal(msg.metadata.identity, 'self', 'msg.metadata.identity') assert.equal(msg.metadata.identityTips, null, 'msg.metadata.identityTips') assert.deepEqual( @@ -51,7 +68,7 @@ test('identity.create() with "keypair" and "domain"', async (t) => { }) assert.ok(identity, 'identity created') const msg = peer.db.get(identity) - assert.equal(msg.data.add, keypair.public, 'msg.data.add') + assert.equal(msg.data.add.key.bytes, keypair.public, 'msg.data.add') assert.equal(msg.metadata.identity, 'self', 'msg.metadata.identity') assert.equal(msg.metadata.identityTips, null, 'msg.metadata.identityTips') assert.deepEqual( @@ -117,8 +134,8 @@ test('identity.findOrCreate() can create', async (t) => { await peer.db.loaded() let gotError = false - await p(peer.db.identity.find)({ keypair, domain }).catch(err => { - assert.equal(err.cause, 'ENOENT'); + await p(peer.db.identity.find)({ keypair, domain }).catch((err) => { + assert.equal(err.cause, 'ENOENT') gotError = true }) assert.ok(gotError, 'identity not found') @@ -126,7 +143,7 @@ test('identity.findOrCreate() can create', async (t) => { const identity = await p(peer.db.identity.findOrCreate)({ keypair, domain }) assert.ok(identity, 'identity created') const msg = peer.db.get(identity) - assert.equal(msg.data.add, keypair.public, 'msg.data.add') + assert.equal(msg.data.add.key.bytes, keypair.public, 'msg.data.add') assert.equal(msg.metadata.identity, 'self', 'msg.metadata.identity') assert.equal(msg.metadata.identityTips, null, 'msg.metadata.identityTips') assert.deepEqual( @@ -138,4 +155,3 @@ test('identity.findOrCreate() can create', async (t) => { await p(peer.close)() }) - diff --git a/test/msg-v3/create.test.js b/test/msg-v3/create.test.js index 00241bd..2e19b52 100644 --- a/test/msg-v3/create.test.js +++ b/test/msg-v3/create.test.js @@ -10,9 +10,23 @@ test('MsgV3.createIdentity()', (t) => { const identityMsg0 = MsgV3.createIdentity(keypair, 'person', 'MYNONCE') console.log(JSON.stringify(identityMsg0, null, 2)) - assert.equal(identityMsg0.data.add, keypair.public, 'data.add') - assert.equal(identityMsg0.metadata.dataHash, 'THi3VkJeaf8aTkLSNJUdFD', 'hash') - assert.equal(identityMsg0.metadata.dataSize, 72, 'size') + assert.deepEqual( + identityMsg0.data, + { + action: 'add', + add: { + key: { + purpose: 'sig', + algorithm: 'ed25519', + bytes: keypair.public, + }, + nonce: 'MYNONCE', + }, + }, + 'data' + ) + assert.equal(identityMsg0.metadata.dataHash, 'C9XZXiZV4kxD6MtVqNkuBw', 'hash') + assert.equal(identityMsg0.metadata.dataSize, 143, 'size') assert.equal(identityMsg0.metadata.identity, 'self', 'identity') assert.equal(identityMsg0.metadata.identityTips, null, 'identityTips') assert.deepEqual(identityMsg0.metadata.tangles, {}, 'tangles') @@ -21,7 +35,7 @@ test('MsgV3.createIdentity()', (t) => { assert.equal(identityMsg0.pubkey, keypair.public, 'pubkey') identity = MsgV3.getMsgHash(identityMsg0) - assert.equal(identity, 'v7vBrnrCTahjgkpoaZrWm', 'identity ID') + assert.equal(identity, 'NwZbYERYSrShDwcKrrLVYe', 'identity ID') }) let rootMsg = null @@ -43,7 +57,7 @@ test('MsgV3.createRoot()', (t) => { assert.equal(rootMsg.pubkey, keypair.public, 'pubkey') rootHash = MsgV3.getMsgHash(rootMsg) - assert.equal(rootHash, 'HPtwPD552ajEurwpgQRfTX', 'root hash') + assert.equal(rootHash, '2yqmqJLffAeomD93izwjx9', 'root hash') }) test('MsgV3.create()', (t) => { @@ -68,7 +82,15 @@ test('MsgV3.create()', (t) => { assert.deepEqual(msg1.data, data, 'data') assert.deepEqual( Object.keys(msg1.metadata), - ['dataHash', 'dataSize', 'identity', 'identityTips', 'tangles', 'domain', 'v'], + [ + 'dataHash', + 'dataSize', + 'identity', + 'identityTips', + 'tangles', + 'domain', + 'v', + ], 'metadata shape' ) assert.deepEqual( @@ -78,14 +100,22 @@ test('MsgV3.create()', (t) => { ) assert.deepEqual(msg1.metadata.dataSize, 23, 'metadata.dataSize') assert.equal(msg1.metadata.identity, identity, 'metadata.identity') - assert.deepEqual(msg1.metadata.identityTips, [identity], 'metadata.identityTips') + assert.deepEqual( + msg1.metadata.identityTips, + [identity], + 'metadata.identityTips' + ) assert.deepEqual( Object.keys(msg1.metadata.tangles), [rootHash], 'metadata.tangles' ) assert.equal(msg1.metadata.tangles[rootHash].depth, 1, 'tangle depth') - assert.deepEqual(msg1.metadata.tangles[rootHash].prev, [rootHash], 'tangle prev') + assert.deepEqual( + msg1.metadata.tangles[rootHash].prev, + [rootHash], + 'tangle prev' + ) assert.equal(msg1.metadata.domain, 'post', 'metadata.domain') assert.deepEqual(msg1.metadata.v, 3, 'metadata.v') assert.equal( @@ -95,11 +125,11 @@ test('MsgV3.create()', (t) => { ) assert.equal( msg1.sig, - '3ucLkFxXJkbX6N7qZQm5PNop2tQ5Z1E9oCVB4HCZjeD3Mn7EXMrgZzCDZfpLTVUUBRqSBQJFxL1j5jNWKFeidHgV', + '2GsXePCkaJk1emgjRmwTrn9qqA5GozG8BrDa9je4SeCNJX8KVYr45MyZGmfkJsGBoMocZCMhP4jiFgdL1PqURhx6', 'sig' ) - const msgHash1 = 'FK4jCKFZDGwecydC8bitgR' + const msgHash1 = 'BTybPnRjVjVZMdWBr52kfz' assert.equal( MsgV3.getMsgId(msg1), @@ -128,7 +158,15 @@ test('MsgV3.create()', (t) => { assert.deepEqual(msg2.data, data2, 'data') assert.deepEqual( Object.keys(msg2.metadata), - ['dataHash', 'dataSize', 'identity', 'identityTips', 'tangles', 'domain', 'v'], + [ + 'dataHash', + 'dataSize', + 'identity', + 'identityTips', + 'tangles', + 'domain', + 'v', + ], 'metadata shape' ) assert.deepEqual( @@ -138,14 +176,22 @@ test('MsgV3.create()', (t) => { ) assert.deepEqual(msg2.metadata.dataSize, 21, 'metadata.dataSize') assert.equal(msg2.metadata.identity, identity, 'metadata.identity') - assert.deepEqual(msg2.metadata.identityTips, [identity], 'metadata.identityTips') + assert.deepEqual( + msg2.metadata.identityTips, + [identity], + 'metadata.identityTips' + ) assert.deepEqual( Object.keys(msg2.metadata.tangles), [rootHash], 'metadata.tangles' ) assert.equal(msg2.metadata.tangles[rootHash].depth, 2, 'tangle depth') - assert.deepEqual(msg2.metadata.tangles[rootHash].prev, [msgHash1], 'tangle prev') + assert.deepEqual( + msg2.metadata.tangles[rootHash].prev, + [msgHash1], + 'tangle prev' + ) assert.equal(msg2.metadata.domain, 'post', 'metadata.domain') assert.deepEqual(msg2.metadata.v, 3, 'metadata.v') assert.equal( @@ -155,13 +201,13 @@ test('MsgV3.create()', (t) => { ) assert.equal( msg2.sig, - 'RtHPPccZNp6c65SnCrfjsNVB6We6G4Ja1oi68AdLuzxSjWNayxepagYJQwgP635E4b55xNGckMiFvJF9Vsn3oAi', + 'EA8PUp44ZBdgsXa4DRioejc4W5NMapoA9oUbgJwQSsvKDj9nh3oAHn7ScnZo2Hfw67JzHeZXeXVwgLCWzsKCFYw', 'sig' ) assert.deepEqual( MsgV3.getMsgId(msg2), - `ppppp:message/v3/${identity}/post/AYXun8rEc3SNGZYM252TAS`, + `ppppp:message/v3/${identity}/post/3bwFna3PvkSNxssPd9QDYe`, 'getMsgId' ) })