detailed tag union for adding identity keys

This commit is contained in:
Andre Staltz 2023-07-14 16:14:00 +03:00
parent 07c2168f97
commit 79bc497911
No known key found for this signature in database
GPG Key ID: 9EDE23EA7E8A4890
7 changed files with 251 additions and 44 deletions

View File

@ -22,6 +22,7 @@ const { decrypt } = require('./encryption')
* @typedef {import('ppppp-keypair').KeypairPublicSlice} KeypairPublicSlice * @typedef {import('ppppp-keypair').KeypairPublicSlice} KeypairPublicSlice
* @typedef {import('ppppp-keypair').KeypairPrivateSlice} KeypairPrivateSlice * @typedef {import('ppppp-keypair').KeypairPrivateSlice} KeypairPrivateSlice
* @typedef {import('./msg-v3').Msg} Msg * @typedef {import('./msg-v3').Msg} Msg
* @typedef {import('./msg-v3').IdentityData} IdentityData
* @typedef {import('./encryption').EncryptionFormat} EncryptionFormat * @typedef {import('./encryption').EncryptionFormat} EncryptionFormat
* *
* @typedef {Buffer | Uint8Array} B4A * @typedef {Buffer | Uint8Array} B4A
@ -287,8 +288,13 @@ function initDB(peer, config) {
} }
for (const msgHash of identityTangle.topoSort()) { for (const msgHash of identityTangle.topoSort()) {
const msg = get(msgHash) const msg = get(msgHash)
if (!msg?.data?.add) continue if (!msg?.data) continue
pubkeys.add(msg.data.add) /** @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) continue
if (!rec.msg) continue if (!rec.msg) continue
if (!rec.msg.data) continue if (!rec.msg.data) continue
if ( if (rec.msg.metadata.identity !== IDENTITY_SELF) continue
rec.msg.metadata.identity === IDENTITY_SELF && if (rec.msg.metadata.domain !== domain) continue
rec.msg.data.add === keypair.public && const data = /** @type {IdentityData} */ (rec.msg.data)
rec.msg.metadata.domain === domain if (data.action === 'add' && data.add.key.bytes === keypair.public) {
) {
const identityId = getIdentityId(rec) const identityId = getIdentityId(rec)
if (identityId) { if (identityId) {
cb(null, identityId) cb(null, identityId)
@ -489,14 +494,26 @@ function initDB(peer, config) {
return cb(new Error('identity.add() failed because the consent is invalid')) 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: // Fill-in tangle opts:
const tangles = populateTangles([opts.identity])
const fullOpts = { const fullOpts = {
identity: IDENTITY_SELF, identity: IDENTITY_SELF,
identityTips: null, identityTips: null,
tangles, tangles: populateTangles([opts.identity]),
keypair: signingKeypair, keypair: signingKeypair,
data: { add: addedKeypair.public, consent: opts.consent }, data,
domain: 'identity', domain: 'identity',
} }

View File

@ -49,6 +49,42 @@ const { IDENTITY_SELF, SIGNATURE_TAG_MSG_V3 } = require('./constants')
* }} Msg * }} Msg
* *
* @typedef {{ * @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; * data: any;
* domain: string; * domain: string;
* keypair: Keypair; * keypair: Keypair;
@ -192,9 +228,21 @@ function getRandomNonce() {
* @returns {Msg} * @returns {Msg}
*/ */
function createIdentity(keypair, domain, nonce = getRandomNonce) { 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({ return create({
data: { add: keypair.public, nonce: actualNonce }, data,
identity: IDENTITY_SELF, identity: IDENTITY_SELF,
identityTips: null, identityTips: null,
keypair, keypair,

View File

@ -33,7 +33,7 @@
"bs58": "~5.0.0", "bs58": "~5.0.0",
"json-canon": "~1.0.0", "json-canon": "~1.0.0",
"obz": "~1.1.0", "obz": "~1.1.0",
"ppppp-keypair": "github:staltz/ppppp-keypair", "ppppp-keypair": "file:../keypair",
"promisify-4loc": "~1.0.0", "promisify-4loc": "~1.0.0",
"push-stream": "~11.2.0", "push-stream": "~11.2.0",
"set.prototype.union": "~1.0.2" "set.prototype.union": "~1.0.2"

View File

@ -47,10 +47,7 @@ Msgs in an identity tangle are special because they have empty `identity` and `i
```typescript ```typescript
interface Msg { interface Msg {
data: { data: IdentityData
add: string // pubkey being added to the identity
nonce?: string // nonce required only on the identity tangle's root
}
metadata: { metadata: {
dataHash: ContentHash dataHash: ContentHash
dataSize: number dataSize: number
@ -68,13 +65,82 @@ interface Msg {
pubkey: Pubkey pubkey: Pubkey
sig: Signature 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:<ID>` where `<ID>` 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: <pubkey>, nonce: <nonce> }` - Registering the first signing pubkey:
- In subsequent messages: `{ add: <pubkey>, consent: <consent> }` ```json
- Where `<consent>` is the base58-encoded signature of the string `:identity-add:<ID>` where `<ID>` is the identity's ID {
"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 ## Feed root

View File

@ -36,7 +36,21 @@ test('identity.add()', async (t) => {
assert.ok(identityRec1, 'identityRec1 exists') assert.ok(identityRec1, 'identityRec1 exists')
const { hash, msg } = identityRec1 const { hash, msg } = identityRec1
assert.ok(hash, 'hash exists') 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.identity, 'self', 'msg.metadata.identity')
assert.equal(msg.metadata.identityTips, null, 'msg.metadata.identityTips') assert.equal(msg.metadata.identityTips, null, 'msg.metadata.identityTips')
assert.deepEqual( assert.deepEqual(

View File

@ -19,10 +19,27 @@ test('identity.create() with just "domain"', async (t) => {
.call(null, { keypair, path: DIR }) .call(null, { keypair, path: DIR })
await peer.db.loaded() 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') assert.ok(identity, 'identityRec0 exists')
const msg = peer.db.get(identity) 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.identity, 'self', 'msg.metadata.identity')
assert.equal(msg.metadata.identityTips, null, 'msg.metadata.identityTips') assert.equal(msg.metadata.identityTips, null, 'msg.metadata.identityTips')
assert.deepEqual( assert.deepEqual(
@ -51,7 +68,7 @@ test('identity.create() with "keypair" and "domain"', async (t) => {
}) })
assert.ok(identity, 'identity created') assert.ok(identity, 'identity created')
const msg = peer.db.get(identity) 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.identity, 'self', 'msg.metadata.identity')
assert.equal(msg.metadata.identityTips, null, 'msg.metadata.identityTips') assert.equal(msg.metadata.identityTips, null, 'msg.metadata.identityTips')
assert.deepEqual( assert.deepEqual(
@ -117,8 +134,8 @@ test('identity.findOrCreate() can create', async (t) => {
await peer.db.loaded() await peer.db.loaded()
let gotError = false let gotError = false
await p(peer.db.identity.find)({ keypair, domain }).catch(err => { await p(peer.db.identity.find)({ keypair, domain }).catch((err) => {
assert.equal(err.cause, 'ENOENT'); assert.equal(err.cause, 'ENOENT')
gotError = true gotError = true
}) })
assert.ok(gotError, 'identity not found') 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 }) const identity = await p(peer.db.identity.findOrCreate)({ keypair, domain })
assert.ok(identity, 'identity created') assert.ok(identity, 'identity created')
const msg = peer.db.get(identity) 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.identity, 'self', 'msg.metadata.identity')
assert.equal(msg.metadata.identityTips, null, 'msg.metadata.identityTips') assert.equal(msg.metadata.identityTips, null, 'msg.metadata.identityTips')
assert.deepEqual( assert.deepEqual(
@ -138,4 +155,3 @@ test('identity.findOrCreate() can create', async (t) => {
await p(peer.close)() await p(peer.close)()
}) })

View File

@ -10,9 +10,23 @@ test('MsgV3.createIdentity()', (t) => {
const identityMsg0 = MsgV3.createIdentity(keypair, 'person', 'MYNONCE') const identityMsg0 = MsgV3.createIdentity(keypair, 'person', 'MYNONCE')
console.log(JSON.stringify(identityMsg0, null, 2)) console.log(JSON.stringify(identityMsg0, null, 2))
assert.equal(identityMsg0.data.add, keypair.public, 'data.add') assert.deepEqual(
assert.equal(identityMsg0.metadata.dataHash, 'THi3VkJeaf8aTkLSNJUdFD', 'hash') identityMsg0.data,
assert.equal(identityMsg0.metadata.dataSize, 72, 'size') {
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.identity, 'self', 'identity')
assert.equal(identityMsg0.metadata.identityTips, null, 'identityTips') assert.equal(identityMsg0.metadata.identityTips, null, 'identityTips')
assert.deepEqual(identityMsg0.metadata.tangles, {}, 'tangles') assert.deepEqual(identityMsg0.metadata.tangles, {}, 'tangles')
@ -21,7 +35,7 @@ test('MsgV3.createIdentity()', (t) => {
assert.equal(identityMsg0.pubkey, keypair.public, 'pubkey') assert.equal(identityMsg0.pubkey, keypair.public, 'pubkey')
identity = MsgV3.getMsgHash(identityMsg0) identity = MsgV3.getMsgHash(identityMsg0)
assert.equal(identity, 'v7vBrnrCTahjgkpoaZrWm', 'identity ID') assert.equal(identity, 'NwZbYERYSrShDwcKrrLVYe', 'identity ID')
}) })
let rootMsg = null let rootMsg = null
@ -43,7 +57,7 @@ test('MsgV3.createRoot()', (t) => {
assert.equal(rootMsg.pubkey, keypair.public, 'pubkey') assert.equal(rootMsg.pubkey, keypair.public, 'pubkey')
rootHash = MsgV3.getMsgHash(rootMsg) rootHash = MsgV3.getMsgHash(rootMsg)
assert.equal(rootHash, 'HPtwPD552ajEurwpgQRfTX', 'root hash') assert.equal(rootHash, '2yqmqJLffAeomD93izwjx9', 'root hash')
}) })
test('MsgV3.create()', (t) => { test('MsgV3.create()', (t) => {
@ -68,7 +82,15 @@ test('MsgV3.create()', (t) => {
assert.deepEqual(msg1.data, data, 'data') assert.deepEqual(msg1.data, data, 'data')
assert.deepEqual( assert.deepEqual(
Object.keys(msg1.metadata), Object.keys(msg1.metadata),
['dataHash', 'dataSize', 'identity', 'identityTips', 'tangles', 'domain', 'v'], [
'dataHash',
'dataSize',
'identity',
'identityTips',
'tangles',
'domain',
'v',
],
'metadata shape' 'metadata shape'
) )
assert.deepEqual( assert.deepEqual(
@ -78,14 +100,22 @@ test('MsgV3.create()', (t) => {
) )
assert.deepEqual(msg1.metadata.dataSize, 23, 'metadata.dataSize') assert.deepEqual(msg1.metadata.dataSize, 23, 'metadata.dataSize')
assert.equal(msg1.metadata.identity, identity, 'metadata.identity') 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( assert.deepEqual(
Object.keys(msg1.metadata.tangles), Object.keys(msg1.metadata.tangles),
[rootHash], [rootHash],
'metadata.tangles' 'metadata.tangles'
) )
assert.equal(msg1.metadata.tangles[rootHash].depth, 1, 'tangle depth') 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.equal(msg1.metadata.domain, 'post', 'metadata.domain')
assert.deepEqual(msg1.metadata.v, 3, 'metadata.v') assert.deepEqual(msg1.metadata.v, 3, 'metadata.v')
assert.equal( assert.equal(
@ -95,11 +125,11 @@ test('MsgV3.create()', (t) => {
) )
assert.equal( assert.equal(
msg1.sig, msg1.sig,
'3ucLkFxXJkbX6N7qZQm5PNop2tQ5Z1E9oCVB4HCZjeD3Mn7EXMrgZzCDZfpLTVUUBRqSBQJFxL1j5jNWKFeidHgV', '2GsXePCkaJk1emgjRmwTrn9qqA5GozG8BrDa9je4SeCNJX8KVYr45MyZGmfkJsGBoMocZCMhP4jiFgdL1PqURhx6',
'sig' 'sig'
) )
const msgHash1 = 'FK4jCKFZDGwecydC8bitgR' const msgHash1 = 'BTybPnRjVjVZMdWBr52kfz'
assert.equal( assert.equal(
MsgV3.getMsgId(msg1), MsgV3.getMsgId(msg1),
@ -128,7 +158,15 @@ test('MsgV3.create()', (t) => {
assert.deepEqual(msg2.data, data2, 'data') assert.deepEqual(msg2.data, data2, 'data')
assert.deepEqual( assert.deepEqual(
Object.keys(msg2.metadata), Object.keys(msg2.metadata),
['dataHash', 'dataSize', 'identity', 'identityTips', 'tangles', 'domain', 'v'], [
'dataHash',
'dataSize',
'identity',
'identityTips',
'tangles',
'domain',
'v',
],
'metadata shape' 'metadata shape'
) )
assert.deepEqual( assert.deepEqual(
@ -138,14 +176,22 @@ test('MsgV3.create()', (t) => {
) )
assert.deepEqual(msg2.metadata.dataSize, 21, 'metadata.dataSize') assert.deepEqual(msg2.metadata.dataSize, 21, 'metadata.dataSize')
assert.equal(msg2.metadata.identity, identity, 'metadata.identity') 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( assert.deepEqual(
Object.keys(msg2.metadata.tangles), Object.keys(msg2.metadata.tangles),
[rootHash], [rootHash],
'metadata.tangles' 'metadata.tangles'
) )
assert.equal(msg2.metadata.tangles[rootHash].depth, 2, 'tangle depth') 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.equal(msg2.metadata.domain, 'post', 'metadata.domain')
assert.deepEqual(msg2.metadata.v, 3, 'metadata.v') assert.deepEqual(msg2.metadata.v, 3, 'metadata.v')
assert.equal( assert.equal(
@ -155,13 +201,13 @@ test('MsgV3.create()', (t) => {
) )
assert.equal( assert.equal(
msg2.sig, msg2.sig,
'RtHPPccZNp6c65SnCrfjsNVB6We6G4Ja1oi68AdLuzxSjWNayxepagYJQwgP635E4b55xNGckMiFvJF9Vsn3oAi', 'EA8PUp44ZBdgsXa4DRioejc4W5NMapoA9oUbgJwQSsvKDj9nh3oAHn7ScnZo2Hfw67JzHeZXeXVwgLCWzsKCFYw',
'sig' 'sig'
) )
assert.deepEqual( assert.deepEqual(
MsgV3.getMsgId(msg2), MsgV3.getMsgId(msg2),
`ppppp:message/v3/${identity}/post/AYXun8rEc3SNGZYM252TAS`, `ppppp:message/v3/${identity}/post/3bwFna3PvkSNxssPd9QDYe`,
'getMsgId' 'getMsgId'
) )
}) })