README
ssc
Static functions for working with a merkle-dag.
This is ssb
but more boring. ssc
because c comes after b in the alphabet
install
npm i @nichoth/ssc
examples using the old node/browser API
sign
Sign a string with a given private key
var ssc = require('@nichoth/ssc')
var test = require('tape')
var _keys
var sig
test('sign a string', function (t) {
_keys = ssc.createKeys()
// _keys here is an object with `.private`, or just a key you want to use
// `{ private: '123' }` or just `123`
var signature = sig = ssc.sign(_keys, 'a test message')
t.ok(signature, 'should return a signature')
t.equal(typeof signature, 'string', 'should return a string')
t.end()
})
verify a signature
Verify a signature with a given public key. _keys
here should have
.public
or can be just a public key: { public: 123 }
or 123
test('verify a signature', t => {
var isValid = ssc.verify(_keys, sig, 'a test message')
t.equal(isValid, true, 'signature verification should work')
t.end()
})
getId
Get the id for a message (the id is the hash of the message)
var ssc = require('@nichoth/ssc')
var keys = ssc.createKeys()
var prev = null
var msg = ssc.createMsg(keys, prev, {
type: 'test',
text: 'ok world'
})
var key = ssc.getId(msg),
generate
var crypto = require('crypto')
var ssc = require('@nichoth/ssc')
var seed = crypto.randomBytes(32)
var keyCap = ssc.generate('ed25519', seed)
// => {
// curve: 'ed25519',
// public: 'U6VgAlPvnSwWs/jocgEOrWjqEPJn6k7RXXog7/jC5zU=.ed25519',
// private: '6iZUnNvLyQHAPobLNA33aavUaljEt06wfuff1iXb9d5TpWACU++dLBaz+OhyAQ6taOoQ8mfqTtFdeiDv+MLnNQ==.ed25519',
// id: '@U6VgAlPvnSwWs/jocgEOrWjqEPJn6k7RXXog7/jC5zU=.ed25519'
// }
create a key pair
var ssc = require('@nichoth/ssc')
var keys = ssc.createKeys()
console.log(keys)
// {
// curve: 'ed25519',
// public: 'd4xotrRAG+l17+r/xXGT1IgHfEzO8fC+5uy/KfFhA0w=.ed25519',
// private: 'ns00cIhaZcZjEdb8vcuEiQ1DyTcfNnuePJBEnnyLqaJ3jGi2tEAb6XXv6v/FcZPUiAd8TM7x8L7m7L8p8WEDTA==.ed25519',
// id: '@d4xotrRAG+l17+r/xXGT1IgHfEzO8fC+5uy/KfFhA0w=.ed25519'
// }
create a message
var ssc = require('@nichoth/ssc')
var keys = ssc.createKeys()
var content = { type: 'test', text: 'woooo' }
// this creates a root message (no ancestors in the merkle list)
// (keys, prevMsg, content)
var msg = ssc.createMsg(keys, null, content)
// => msg:
// {
// previous: null,
// sequence: 1,
// author: '@IGrkmx/GjfzaOLNjTpdmmPWuTj5xeSv/2pCP+yUI8eo=.ed25519',
// timestamp: 1608054728047,
// hash: 'sha256',
// content: { type: 'test', text: 'woooo' },
// signature: 'LJUQXvR6SZ9lQSlF1w1RFQi3GFIU4B/Cc1sP6kjxnMZn3YW8X7nj9/hlWiTF3cJbWkc9xHvApJ+9uRtHxicXAQ==.sig.ed25519'
// }
// pass in prev msg to create the merkle-list
var msg2 = ssc.createMsg(keys, msg, content)
verify a message
This checks that the signature and public key are ok together.
var ssc = require('@nichoth/ssc')
// keys = { public }
// function verifyObj (keys, hmac_key, obj)
var msgIsOk = ssc.verifyObj(keys, null, msg)
// true
create another message
The new message contains the previous message's hash, and is signed by the message author
var ssc = require('@nichoth/ssc')
var content2 = { type: 'test2', text: 'ok' }
// we pass in the original msg here
var msg2 = ssc.createMsg(keys, msg, content2)
(msg2.previous === ssc.getId(msg))
// => true
isValidMsg
This checks that the message contains the hash of prevMsg
, and also
makes sure the signature is valid.
var ssc = require('@nichoth/ssc')
var msg = ssc.createMsg(keys, null, { type: 'test', text: 'ok' })
var msg2 = ssc.createMsg(keys, msg, { type: 'test', text: 'ok' })
// (msg, prevMsg, keys)
var isValid = ssc.isValidMsg(msg2, msg, keys)
// => true
var badMsg = ssc.createMsg(keys, null, { type: 'test', text: 'ok' })
var isOk = ssc.isValidMsg(badMsg, msg2, keys)
// => false
// we pass in null as the prevMsg, but validate with msg2 as prev
Create a merkle list from an array
var ssc = require('@nichoth/ssc')
var keys = ssc.createKeys()
test('create a merkle list', function (t) {
t.plan(2)
var arr = ['one', 'two', 'three']
var list = arr.reduce(function (acc, val) {
var prev = (acc[acc.length - 1] || null)
var msg = ssc.createMsg(keys, prev, {
type: 'test',
text: val
})
acc.push(msg)
return acc
}, [])
t.equal(list.length, 3, 'should create a merkle list')
var isValidList = list.reduce(function (isValid, msg, i) {
var prev = list[i - 1] || null
// ssc.isValidMsg(msg2, msg, keys)
return isValid && ssc.isValidMsg(msg, prev, keys)
}, true)
t.equal(isValidList, true, 'reduced validation should be ok')
})
hash
Get the hash of a string or buffer
var ssc = require('@nichoth/ssc')
var hash = ssc.hash('a string to hash')
// 'GHx6bNkCvFIPAwFVUNc1qOJPAPiIwDKMm2vL0tfJDPc=.sha256'
create ssb style post messages
// messages have { key, value }
var ssc = require('@nichoth/ssc')
test('create ssb style posts', function (t) {
t.plan(3)
var arr = ['one', 'two', 'three']
var list = arr.reduce(function (acc, val) {
var prev = (acc[acc.length - 1] || null)
// need to use the `.value` key in this case
prev = prev === null ? prev : prev.value
var msg = ssc.createMsg(keys, prev, {
type: 'test',
text: val
})
acc.push({
key: ssc.getId(msg),
value: msg
})
return acc
}, [])
t.ok(list[0].key, 'should have `.key`')
t.ok(ssc.verifyObj(keys, null, list[0].value),
'msg should have valid .value')
t.equal(list[0].value.content.text, 'one',
'should have the right content at the right key')
t.equal(list[0].key[0], '%', 'should have the right format id')
})
notes
ssb format is { key: '...', value: msg }
. I think this is just used for storing in the DB though, where key is the hash or something of the message.
// ssb style post
// {
// key: '%uS0xrYDtij+ukWHir98G8cdCo8sgGDp4t2HoBWUYl3Q=.sha256',
// value: {
// previous: '%Qh2prm1RsxOjYSb0Qp9KNFjp641sL4MJGfnd8jAE3N8=.sha256',
// sequence: 3,
// author: '@FppHFxGG2TO2HqLGVad1VIwcFlTu9okR5qqj5ejGXFk=.ed25519',
// timestamp: 1586138755567,
// hash: 'sha256',
// content: { type: 'ev.post', text: 'iguana', mentions: [Array] },
// signature: 'iDWUHP/v31LELJ9PRkuPA/12IDwltHRUNRYQ0YkRUrr9wPCgi/VUzNrUmid7N64TYjeV6dL9dUx5ESShTsiqCg==.sig.ed25519'
// },
// timestamp: 1586138755568
//}
Example using the Web Crypto API
var ssc = require('@nichoth/ssc/web')
var test = require('tape')
var ks
test('create keys', async t => {
ks = await ssc.createKeys()
t.ok(ks, 'should return a keystore')
t.end()
})
test('sign and validate something', async t => {
var sig = await ks.sign('my message')
t.ok(sig, 'should sign a message')
const publicKey = await ks.publicWriteKey()
var isValid = await ks.verify('my message', sig, publicKey)
t.equal(isValid, true, 'should be a valid signature')
})
var msg
test('create a message', async t => {
var content = { type: 'test', text: 'woooo' }
msg = await ssc.createMsg(ks, null, content)
t.ok(msg, 'should create a message')
t.ok(msg.author, 'should have the message author')
t.equal(msg.content.type, 'test', 'should have the message content')
t.ok(msg.signature, 'should have the message signature')
t.end()
})
// This checks that the signature and given public key are valid
test('verify a message', async t => {
var msgIsOk = await ssc.verifyObj(ks, msg)
t.equal(msgIsOk, true, 'should return true for a valid message')
t.end()
})
// this checks the merkle-ness, in addition to the signature being valid
test('is valid message', async t => {
// (msg, prevMsg, keys)
var isValid = await ssc.isValidMsg(msg, null, ks)
t.plan(1)
t.equal(isValid, true, 'should return true for valid message')
})
var msg2
test('create a second message', async t => {
t.plan(1)
var content2 = { type: 'test2', text: 'ok' }
// we pass in the original msg here
msg2 = await ssc.createMsg(ks, msg, content2)
t.ok(msg2.previous === ssc.getId(msg),
'should create `prev` as prev msg hash')
// => true
})
// check that the message contains the hash of prevMsg, and also makes sure
// the signature is valid
test('validate the second message', async t => {
// (msg, prevMsg, keys)
var isValid = await ssc.isValidMsg(msg2, msg, ks)
t.equal(isValid, true, 'should validate a message with a previous hash')
t.end()
})
// this works but is kind of confusing because of the use of promises &
// async functions. I should re-do this
test('create a merkle list', async t => {
t.plan(2)
var arr = ['one', 'two', 'three']
var list = await arr.reduce(async function (acc, val) {
return acc.then(async res => {
var prev = res[res.length - 1]
if (!res[res.length - 1]) prev = null
return ssc.createMsg(ks, prev, { type: 'test', text: val })
.then(result => {
res.push(result)
return res
})
})
}, Promise.resolve([]))
t.equal(list.length, 3, 'should create the right number of list items')
var isValidList = await list.reduce(async function (isValid, msg, i) {
var prev = list[i - 1] || null
// ssc.isValidMsg(msg2, msg, keys)
return isValid && await ssc.isValidMsg(msg, prev, ks)
}, true)
t.equal(isValidList, true, 'reduced validation should be ok')
})
test('get the DID from a set of keys', async t => {
var auth = await ssc.getDidFromKeys(ks)
t.equal(auth, ssc.getAuthor(msg),
'should get the author DID from a set of keys')
t.end()
})
UCAN notes
ssc UCAN How to do invitations?
A given user should be able to invite another user
UserA creates a UCAN for userB. That means userA must have a DID for userB.
UserA enters an email address. The system sends an email to the address, the email links to a page with a secret code used to match the new user to an invitation.
When you redeem an invitation
- It creates a DID for you
- check to see if the invitation code is ok
- iff the invitation is ok, then the server follows userB, and can create a DB record that userA follows userB
need to create a UCAN for userB. Use a given UCAN as theproof
in the new user's UCAN. This means you need to save the userA UCAN somewhere (since they created the invitation).
Does that work though? You need a signature
on any ucan
New UCANs need to be signed by the issuer. That means you need to know the DID of the audience and also have the keys of the issuer when you create a UCAN.
Can have a UCAN for the server, and the server will create a UCAN for the invited user iff the invitation code is valid. So then the server is following the userB. Also should create a DB record where userA follows userB.
The UCAN must be signed with the private key of the issuer to be valid
Here the issuer is the server.
Note that the ucans
module has a browser
field in package.json
, which means that it import the right file (dist/index.js
) when you import it then compile with esmify
UCANs are a merkle-list of signed objects for user permissions. This is better than just a DID because it adds some additional fields related to permissions.
The DID
here is a decentralized identifier
You call the wn.ucan.build
method:
wn.ucan.build({
// Audience, the ID of who it's intended for
// who this UCAN describes
audience: otherDID,
// the issuer always has to be your DID, because the UCAN will be
// signed with your private key
// the ID of who sent this
issuer: ourDID,
// `facts` can be used for arbitrary data
// facts: [],
lifetimeInSeconds: 60 * 60 * 24, // UCAN expires in 24 hours
// `potency` is used by our application
potency: 'APPEND_ONLY',
proof: possibleProof
})
.then((ucan) => {})
proof
in the arguments above is another UCAN. Your application would check
that the UCAN in proof
is valid, and that it is allowed to give the
specified permissions to the audience
user.
You would pass it an 'encoded' UCAN:
possibleProof = wn.ucan.encode(otherUcan)
ucan.build
returns an object like:
{
"header": {
"alg": "RS256",
"typ": "JWT",
"uav": "1.0.0"
},
"payload": {
"aud": "did:key:EXAMPLE",
"exp": 1631811852,
"fct": [],
"iss": "did:key:z13V3Sog...",
"nbf": 1631811763,
"prf": "eyJhbGciOiJSU...",
"ptc": "APPEND_ONLY",
"rsc": "*"
},
"signature": "NtlF3wOoVLlZo..."
}
In our application, we check that the UCAN is valid:
wn.ucan.isValid(ucan)
Then you want to check that the proof(s) are valid:
if (ucan.prf) {
wn.ucan.isValid(wn.ucan.decode(ucan.prf))
}
You also need to check the permissions -- the potency
field, and make sure
that the given proof
is allowed to issue the given permissions.
This forms a chain of UCANs and proof
UCANs. The final validation of
permissions would happen out of band from the UCAN chain. Meaning, if
there's no proof field, then we need to lookup the audience
in the UCAN
and verify that their permissions are ok.
keystore.init
=>
ECCkeystore.init
=>
IDB.createStore
=>
var store = localforage.createInstance
=>
fn = keys.makeKeypair = () => crypto.subtle.generateKey(
keys.makeKeypair =
IDB.createIfDoesNotExist(_, fn, store)