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').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',
}

View File

@ -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,

View File

@ -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"

View File

@ -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:<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> }`
- In subsequent messages: `{ add: <pubkey>, consent: <consent> }`
- Where `<consent>` is the base58-encoded signature of the string `:identity-add:<ID>` where `<ID>` 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

View File

@ -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(

View File

@ -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)()
})

View File

@ -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'
)
})