├── .github └── stale.yml ├── .gitignore ├── .npmrc ├── .travis.yml ├── LICENSE ├── README.md ├── example.js ├── index.js ├── package.json └── test ├── append-ooo.js ├── data.js ├── error.js └── generate.js /.github/stale.yml: -------------------------------------------------------------------------------- 1 | _extends: .github 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8 4 | - 10 5 | - 11 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 'Dominic Tarr' 2 | 3 | Permission is hereby granted, free of charge, 4 | to any person obtaining a copy of this software and 5 | associated documentation files (the "Software"), to 6 | deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, 8 | merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom 10 | the Software is furnished to do so, 11 | subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 20 | ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ssb-validate 2 | 3 | validate ssb messages, completely functionally. 4 | 5 | this module seeks to replace ssb-feed, and contain the logic to validate ssb messages, 6 | but this is implemented functionally, with serializable state. 7 | this means, that the states generated when the reference implementation runs 8 | can be extracted and used as test cases in other implementations. 9 | 10 | ## example 11 | 12 | ``` js 13 | var validate = require('ssb-validate') 14 | var hmac_key = null 15 | var state = validate.initial() 16 | 17 | var msgs = [...] //some source of messages 18 | 19 | msgs.forEach(function (msg) { 20 | try { 21 | state = validate.append(state, hmac_key, msg) 22 | } catch (err) { 23 | console.log(err) 24 | } 25 | }) 26 | 27 | writeToDatabase(state.queue, function (err) { 28 | if(err) throw err 29 | 30 | //these messages are fully accepted now, can remove them from state. 31 | state.queue = [] 32 | }) 33 | 34 | //state should be saved in some way it can be reconstructed 35 | //so in the future it can be appended to starting from scratch. 36 | ``` 37 | 38 | ## architecture 39 | 40 | This module describes the validation logic for ssb messages, using a reduce style 41 | pattern - `state = method(state, argument)` . To make it easy to test, the various 42 | subcomponents are also exported, but the main method is `state = append(state, hmac_key, msg)` 43 | 44 | ## state 45 | 46 | this module uses a data structure to represent the state of validation. 47 | The structure is as follows: 48 | 49 | ``` js 50 | { 51 | validated: integer, 52 | queued: integer, 53 | queue: [kvt...], //messages which have been validated and may now be written to database 54 | feeds: { //validation state of all feeds 55 | : { 56 | id: , 57 | timestamp: , 58 | sequence: integer // 59 | queue: [kvt...] 60 | },... 61 | } 62 | } 63 | ``` 64 | note: `kvt` here represents a msg with key calculated and a recieved timestamp assigned 65 | `{key: hash(msg), value: msg, timestamp: timestamp()}` 66 | 67 | ## api 68 | 69 | ### state = validate.append(state, hmac_key, msg) 70 | 71 | Append and validate a message to state. If the message is valid, it will appear at the 72 | end of `state.queue` it may now be written to the database. 73 | 74 | `hmac_key` is optional - if provided, messages are hmac'd with it before signing, 75 | which can be used to create a separate network in which messages cannot be copied 76 | to the main network. Messages compatible with the main network have `hmac_key = null` 77 | 78 | ### msg = validate.create(feed_state, keys, hmac_key, content, timestamp) 79 | 80 | Create a message that is valid given the current state of the feed (such that it may be passed to 81 | `append(state, hmac_key, msg)`. `keys` is the signing key, as provided by `ssbKeys.generate()` or `ssbKeys.createOrLoadSync(filename)` 82 | 83 | ### msg_id = validate.id(msg) 84 | 85 | Calculate the message id for a given message. 86 | 87 | ## shortcuts for js mode api 88 | 89 | In investigating the possiblity of an entirely web based scuttlebutt, 90 | verifying all signatures in javascript had added a lot of overhead. 91 | (although now this is not such a problem because of crypto in fast-enough webassembly) 92 | 93 | The following methods queues some number of messages and then validates the last signature. 94 | It is possible that a specially constructed messages with some invalid signatures, followed 95 | by messages with valid signatures could get accepted, but the writer doesn't have any way 96 | to know _which_ messages will be accepted, and a 3rd party could not insert invalid messages 97 | into another feed, because the signature that is eventually checked wouldn't point to the right 98 | previous hash. 99 | 100 | However, you can probably consider these methods not necessary. And they could be removed. 101 | 102 | 103 | ``` 104 | var validate = require('ssb-validate') 105 | var hmac_key = null 106 | var state = validate.initial() 107 | 108 | var msgs = [...] //some source of messages 109 | 110 | //queue messages 111 | msgs.forEach(function (msg) { 112 | state = validate.queue(state, msg) 113 | if(state.error) console.error(state.error) 114 | }) 115 | 116 | //validate messages 117 | 118 | for(var feed_id in state.feeds) 119 | state = validate.validate(state, hmac_key, feed_id) 120 | 121 | writeToDatabase(state.queue, function (err) { 122 | if(err) throw err 123 | 124 | //these messages are fully accepted now, can remove them from state. 125 | state.queue = [] 126 | }) 127 | 128 | //state should be saved in some way it can be reconstructed 129 | //so in the future it can be appended to starting from scratch. 130 | ``` 131 | 132 | ### state = validate.queue(state, msg) 133 | 134 | Call checkInvalidCheap and if valid, append to the feed's incoming queue. 135 | (`state.feeds[id(msg)].queue`) 136 | 137 | The message is checked to have an incrementing `sequence`, and correct `previous` hash, 138 | but signature is not checked. (the intention here is for when using javascript crypto, 139 | checking every signature is expensive) however, this is not a big problem with wasm crypto. 140 | 141 | ### state = validate.validate(state, feed_id) 142 | 143 | Check the signature of the last message in feed_id's incoming queue, 144 | and if it is valid, append all messages in that queue. 145 | As optimization/shortcut for javascript crypto. 146 | 147 | ### state = validate.appendOOO(state, hmac_key, msg) 148 | 149 | This method works the same as append except that it does not check the 150 | previous link of the message. This allows one to validate an out of 151 | order message or a row of messages not starting from the beginning. 152 | 153 | ## internal api 154 | 155 | The following methods are exposed for testing, but are unlikely to be used directly. 156 | 157 | ### state = validate.appendNew(state,hmac_key, keys, content, timestamp) 158 | 159 | Wrapper around create and append. used in testing. 160 | 161 | ### state = validate.appendKVT (state, hmac_key, kvt) 162 | 163 | Internal details of append - recently this was refactored to avoid calculating 164 | the message id twice. 165 | 166 | ### isInvalid = validate.checkInvalidCheap (state, msg) 167 | 168 | Perform cheap checks for message validity, but not the signature. 169 | return false if the message is valid, and an error (with message) if it's invalid. 170 | 171 | ### isInvalid = validate.checkInvalid (state, hmac_key, msg) 172 | 173 | Check signature, returns false if message is valid. returns an error (with message) 174 | if the message is invalid. 175 | 176 | ## License 177 | 178 | MIT 179 | 180 | 181 | 182 | 183 | 184 | 185 | -------------------------------------------------------------------------------- /example.js: -------------------------------------------------------------------------------- 1 | var pull = require('pull-stream') 2 | 3 | var v = require('./') 4 | var state = { 5 | queue: [], 6 | feeds: {}, 7 | error: null 8 | } 9 | 10 | var c = 0, e = 0, start = Date.now() 11 | require('ssb-client')(function (err, sbot) { 12 | if(err) throw err 13 | pull( 14 | sbot.createLogStream(), 15 | pull.drain(function (msg) { 16 | state = v.append(state, null, msg.value) 17 | if(state.error) { 18 | e++ 19 | var err = state.error 20 | state.error = null 21 | console.log(err.message) 22 | return false 23 | } 24 | state.queue.shift() 25 | var s = ((Date.now() - start)/1000) 26 | if(!(c++%1000)) { 27 | console.log(s, e, c, c / s) 28 | } 29 | return true 30 | }, sbot.close) 31 | ) 32 | }) 33 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var ref = require('ssb-ref') 2 | var ssbKeys = require('ssb-keys') 3 | var isFeedId = ref.isFeedId 4 | var timestamp = require('monotonic-timestamp') 5 | var isCanonicalBase64 = require('is-canonical-base64') 6 | var isEncryptedRx = isCanonicalBase64('','\\.box.*') 7 | var isSignatureRx = isCanonicalBase64('','\\.sig.\\w+') 8 | 9 | function isValidOrder (msg, signed) { 10 | var keys = Object.keys(msg) 11 | if(signed && keys.length !== 7) return false 12 | if( 13 | keys[0] !== 'previous' || 14 | keys[3] !== 'timestamp' || 15 | keys[4] !== 'hash' || 16 | keys[5] !== 'content' || 17 | (signed && keys[6] !== 'signature') 18 | ) return false 19 | //author and sequence may be swapped. 20 | if(!( 21 | (keys[1] === 'sequence' && keys[2] === 'author') || 22 | (keys[1] === 'author' && keys[2] === 'sequence') 23 | )) 24 | return false 25 | return true 26 | } 27 | 28 | var encode = exports.encode = function (obj) { 29 | return JSON.stringify(obj, null, 2) 30 | } 31 | 32 | exports.initial = function () { 33 | return { 34 | validated: 0, 35 | queued: 0, 36 | queue: [], 37 | feeds: {}, 38 | error: null 39 | } 40 | } 41 | 42 | 43 | function isString (s) { 44 | return s && 'string' === typeof s 45 | } 46 | 47 | function isInteger (n) { 48 | return ~~n === n 49 | } 50 | 51 | function isObject (o) { 52 | return o && 'object' === typeof o 53 | } 54 | 55 | function isEncrypted (str) { 56 | //NOTE: does not match end of string, 57 | //so future box version are accepted. 58 | //XXX check that base64 is canonical! 59 | return isString(str) && isEncryptedRx.test(str) ///^[0-9A-Za-z\/+]+={0,2}\.box/.test(str) 60 | } 61 | 62 | var isInvalidContent = exports.isInvalidContent = function (content) { 63 | if(!isEncrypted(content)) { 64 | var type = content.type 65 | if (!(isString(type) && type.length <= 52 && type.length >= 3)) { 66 | return new Error('type must be a string' + 67 | '3 <= type.length < 52, was:' + type 68 | ) 69 | } 70 | } 71 | return false 72 | } 73 | 74 | var isSupportedHash = exports.isSupportedHash = function (msg) { 75 | return msg.hash === 'sha256' 76 | } 77 | 78 | var isSigMatchesCurve = exports.isSigMatchesCurve = function (msg) { 79 | if(!isSignatureRx.test(msg.signature)) return 80 | var curve = /\.(\w+)/.exec(msg.author) 81 | if(!(curve && curve[1])) return 82 | 83 | const signatureBase64Length = msg.signature.length - (curve[1].length + 5) 84 | 85 | if (signatureBase64Length !== 88) return 86 | if ('.sig.'+curve[1] !== msg.signature.substring(signatureBase64Length)) return 87 | 88 | return true 89 | } 90 | 91 | var isInvalidShape = exports.isInvalidShape = function (msg) { 92 | if( 93 | !isObject(msg) || 94 | !isInteger(msg.sequence) || 95 | !isFeedId(msg.author) || 96 | !(isObject(msg.content) || isEncrypted(msg.content)) || 97 | !isValidOrder(msg, false) || //false, because message may not be signed yet. 98 | !isSupportedHash(msg) 99 | ) 100 | return new Error('message has invalid properties:'+JSON.stringify(msg, null, 2)) 101 | 102 | //allow encrypted messages, where content is a base64 string. 103 | 104 | //NOTE: `.length` returns the number of utf-16 code units 105 | //and NOT the byte length! For a latin1 string, the utf-16 106 | //length will be equal to the byte length (each character is one byte). 107 | //However, utf-8 strings may have a greater byte length 108 | //than the utf-16 length. This means that a message can be 109 | //larger than 8 KB and still pass this validation check! 110 | //This is a weird legacy thing, obviously, that we will fix at some point... 111 | var asJson = encode(msg) 112 | if (asJson.length > 8192) // utf-16 code units, NOT bytes! 113 | return new Error('Encoded message must not be larger than 8192 bytes. Current size is '+asJson.length) 114 | 115 | return isInvalidContent(msg.content) 116 | } 117 | 118 | const isInvalidHmacKey = (hmacKey) => { 119 | if (hmacKey === undefined) return false 120 | if (hmacKey === null) return false 121 | const bytes = Buffer.isBuffer(hmacKey) ? hmacKey : Buffer.from(hmacKey, 'base64') 122 | 123 | if (typeof hmacKey === 'string') { 124 | if (bytes.toString('base64') !== hmacKey) return true 125 | } 126 | 127 | if (bytes.length !== 32) return true 128 | return false 129 | } 130 | 131 | function fatal(err) { 132 | err.fatal = true 133 | return err 134 | } 135 | 136 | exports.checkInvalidCheap = function (state, msg) { 137 | //the message is just invalid 138 | if(!ref.isFeedId(msg.author)) 139 | return new Error('invalid message: must have author') 140 | if(!isSigMatchesCurve(msg)) 141 | return new Error('invalid message: signature type must match author type') 142 | 143 | //state is id, sequence, timestamp 144 | if(state) { 145 | //most likely, we just tried to append two messages twice 146 | //or append another message after an error. 147 | if(msg.sequence != state.sequence + 1) 148 | return new Error('invalid message: expected sequence ' + (state.sequence + 1) + ' but got:'+ msg.sequence + 'in state:'+JSON.stringify(state)+', on feed:'+msg.author) 149 | //if we have the correct sequence and wrong previous, 150 | //this must be a fork! 151 | if(msg.previous != state.id) 152 | return fatal(new Error('invalid message: expected different previous message, on feed:'+msg.author)) 153 | //and check type, and length, and some other stuff. finally check the signature. 154 | } 155 | else { 156 | if(msg.previous !== null) 157 | return fatal(new Error('initial message must have previous: null, on feed:'+msg.author)) 158 | if(msg.sequence !== 1) 159 | return fatal(new Error('initial message must have sequence: 1, on feed:'+msg.author)) 160 | if('number' !== typeof msg.timestamp) 161 | return fatal(new Error('initial message must have timestamp, on feed:'+msg.author)) 162 | } 163 | if(!isValidOrder(msg, true)) 164 | return fatal(new Error('message must have keys in allowed order')) 165 | 166 | return isInvalidShape(msg) 167 | } 168 | 169 | exports.checkInvalid = function (state, hmac_key, msg) { 170 | var err = exports.checkInvalidCheap(state, msg) 171 | if(err) return err 172 | 173 | if (isInvalidHmacKey(hmac_key)) { 174 | return fatal(new Error('invalid HMAC key')) 175 | } 176 | if(!ssbKeys.verifyObj({public: msg.author.substring(1)}, hmac_key, msg)) 177 | return fatal(new Error('invalid signature')) 178 | return false //not invalid 179 | } 180 | 181 | /* 182 | { 183 | //an array of messages which have been validated, but not written to the database yet. 184 | valid: [], 185 | //a map of information needed to know if something should be appeneded to the valid queue. 186 | feeds: { 187 | : {id, sequence, ts} 188 | }, 189 | error: null 190 | } 191 | */ 192 | 193 | exports.queue = function (state, msg) { 194 | state.error = exports.checkInvalidCheap(flatState(state.feeds[msg.author]), msg) 195 | 196 | if(state.error) 197 | return state 198 | state.feeds[msg.author] = state.feeds[msg.author] || { 199 | id: null, sequence: null, timestamp: null, queue: [] 200 | } 201 | state.queued += 1 202 | state.feeds[msg.author].queue.push(exports.toKeyValueTimestamp(msg)) 203 | return state 204 | } 205 | 206 | exports.toKeyValueTimestamp = function (msg, id) { 207 | return { 208 | key: id ? id : exports.id(msg), 209 | value: msg, 210 | timestamp: timestamp() 211 | } 212 | } 213 | 214 | function flatState (fstate) { 215 | if(!fstate) return null 216 | if(fstate.queue.length) { 217 | var last = fstate.queue[fstate.queue.length - 1] 218 | return { 219 | id: last.key, 220 | timestamp: last.value.timestamp, 221 | sequence: last.value.sequence 222 | } 223 | } 224 | else 225 | return fstate 226 | } 227 | 228 | exports.appendKVT = function (state, hmac_key, kvt) { 229 | var err 230 | var msg_id = kvt.key 231 | var msg = kvt.value 232 | var _state = flatState(state.feeds[msg.author]) 233 | err = exports.checkInvalid(_state, hmac_key, msg) 234 | 235 | if(err) 236 | throw err 237 | else if(state.feeds[msg.author]) { 238 | var a = state.feeds[msg.author] 239 | a.id = msg_id 240 | a.sequence = msg.sequence 241 | a.timestamp = msg.timestamp 242 | var q = state.feeds[msg.author].queue 243 | state.validated += q.length 244 | state.queued -= q.length 245 | for (var i = 0; i < q.length; ++i) 246 | state.queue.push(q[i]) 247 | q = [] 248 | } 249 | else if(msg.sequence === 1) { 250 | state.feeds[msg.author] = { 251 | id: msg_id, 252 | sequence: msg.sequence, 253 | timestamp: msg.timestamp, 254 | queue: [] 255 | } 256 | } 257 | 258 | state.queue.push(kvt) 259 | state.validated += 1 260 | return state 261 | } 262 | 263 | exports.append = function (state, hmac_key, msg) { 264 | return exports.appendKVT(state, hmac_key, exports.toKeyValueTimestamp(msg)) 265 | } 266 | 267 | exports.checkInvalidOOO = function(msg, hmac_key) { 268 | if(!ref.isFeedId(msg.author)) 269 | return new Error('invalid message: must have author') 270 | if(!isSigMatchesCurve(msg)) 271 | return new Error('invalid message: signature type must match author type') 272 | if('number' !== typeof msg.timestamp) 273 | return fatal(new Error('message must have timestamp, on feed:'+msg.author)) 274 | if(!isValidOrder(msg, true)) 275 | return fatal(new Error('message must have keys in allowed order')) 276 | if (isInvalidShape(msg)) 277 | return fatal(new Error('message has invalid shape')) 278 | if (isInvalidHmacKey(hmac_key)) 279 | return fatal(new Error('invalid HMAC key')) 280 | if(!ssbKeys.verifyObj({public: msg.author.substring(1)}, hmac_key, msg)) 281 | return fatal(new Error('invalid signature')) 282 | 283 | return false // ok 284 | } 285 | 286 | exports.appendOOO = function(state, hmac_key, msg) { 287 | 288 | state.error = exports.checkInvalidOOO(msg, hmac_key) 289 | if (state.error) 290 | return state 291 | 292 | var kvt = exports.toKeyValueTimestamp(msg) 293 | 294 | if (!state.feeds[msg.author]) { 295 | state.feeds[msg.author] = { 296 | id: kvt.key, 297 | sequence: msg.sequence, 298 | timestamp: msg.timestamp, 299 | queue: [] 300 | } 301 | } 302 | 303 | state.queue.push(kvt) 304 | state.validated += 1 305 | return state 306 | } 307 | 308 | exports.validate = function (state, hmac_key, feed) { 309 | if(!isFeedId(feed)) throw new Error('validate takes a feedId') 310 | if(!state.feeds[feed] || !state.feeds[feed].queue.length) { 311 | return state 312 | } 313 | var kvt = state.feeds[feed].queue.pop() 314 | state.queued -= 1 315 | return exports.appendKVT(state, hmac_key, kvt) 316 | } 317 | 318 | //pass in your own timestamp, so it's completely deterministic 319 | exports.create = function (state, keys, hmac_key, content, timestamp) { 320 | if(timestamp == null || isNaN(+timestamp)) throw new Error('timestamp must be provided') 321 | 322 | if(!isObject(content) && !isEncrypted(content)) 323 | throw new Error('invalid message content, must be object or encrypted string') 324 | 325 | state = flatState(state) 326 | 327 | var msg = { 328 | previous: state ? state.id : null, 329 | sequence: state ? state.sequence + 1 : 1, 330 | author: keys.id, 331 | timestamp: +timestamp, 332 | hash: 'sha256', 333 | content: content 334 | } 335 | 336 | var err = isInvalidShape(msg) 337 | if(err) throw err 338 | return ssbKeys.signObj(keys, hmac_key, msg) 339 | } 340 | 341 | exports.id = function (msg) { 342 | return '%'+ssbKeys.hash(JSON.stringify(msg, null, 2)) 343 | } 344 | 345 | exports.appendNew = function (state, hmac_key, keys, content, timestamp) { 346 | var msg = exports.create(state.feeds[keys.id], keys, hmac_key, content, timestamp) 347 | state = exports.append(state, hmac_key, msg) 348 | return state 349 | } 350 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssb-validate", 3 | "description": "simplified validation for secure-scuttlebutt", 4 | "version": "4.1.4", 5 | "homepage": "https://github.com/ssbc/ssb-validate", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/ssbc/ssb-validate.git" 9 | }, 10 | "dependencies": { 11 | "is-canonical-base64": "^1.1.1", 12 | "monotonic-timestamp": "0.0.9", 13 | "ssb-keys": "^8.1.0", 14 | "ssb-ref": "^2.6.2" 15 | }, 16 | "devDependencies": { 17 | "pull-stream": "^3.6.0", 18 | "ssb-client": "^4.5.0", 19 | "ssb-validation-dataset": "^1.2.1", 20 | "tape": "^4.6.3" 21 | }, 22 | "scripts": { 23 | "test": "set -e; for t in test/*.js; do node $t; done" 24 | }, 25 | "author": "'Dominic Tarr' (http://dominictarr.com)", 26 | "license": "MIT" 27 | } 28 | -------------------------------------------------------------------------------- /test/append-ooo.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var ssbKeys = require('ssb-keys') 3 | 4 | var seed = require('crypto').createHash('sha256').update('validation-test-seed').digest() 5 | var keys = ssbKeys.generate('ed25519', seed) 6 | 7 | var v = require('../') 8 | 9 | tape('append ooo', function (t) { 10 | var hmac_key = null 11 | 12 | var state = v.initial() 13 | var msg = v.create(null, keys, hmac_key, {type: 'test'}, +new Date('2017-04-11 9:09 UTC')) 14 | 15 | state = v.append(state, hmac_key, msg) 16 | t.equal(state.validated, 1) 17 | 18 | var msg2 = v.create(state, keys, hmac_key, {type: 'test2'}, +new Date('2017-04-11 9:10 UTC')) 19 | 20 | var stateOOO = v.initial() 21 | 22 | stateOOO = v.appendOOO(stateOOO, hmac_key, msg2) 23 | t.equal(stateOOO.validated, 1) 24 | 25 | var invalidSignature = { 26 | previous: '%u5CkR2ik8jHMJFf0VY8STAY2+ou8C9kpRvmGOUEdr8A=.sha256', 27 | sequence: 2, 28 | author: '@dGm2+y3z0PCjt2Q08ruSFa7yh11g755dxZNjXWwxp90=.ed25519', 29 | timestamp: 1491901800000, 30 | hash: 'sha256', 31 | content: { type: 'test2' }, 32 | signature: 33 | '/HAXhrhqHU6Zcmd3+CdiHgaoloXiVGPK3hB+6EiwoaMuC3PHv8TwfenWf8GIqptSrPJATyJfsdW1sMinqpirDA==.sig.ed25519' 34 | } 35 | 36 | var stateOOOSigError = v.appendOOO(v.initial(), hmac_key, invalidSignature) 37 | t.equal(stateOOOSigError.validated, 0) 38 | t.equal(stateOOOSigError.error.message, 'invalid signature') 39 | 40 | var missingPrevious = { 41 | sequence: 2, 42 | author: '@dGm2+y3z0PCjt2Q08ruSFa7yh11g755dxZNjXWwxp90=.ed25519', 43 | timestamp: 1491901800000, 44 | hash: 'sha256', 45 | content: { type: 'test2' }, 46 | signature: 47 | '/IGohrhqHU6Zcmd3+CdiHgaoloXiVGPK3hB+6EiwoaMuC3PHv8TwfenWf8GIqptSrPJATyJfsdW1sMinqpirDA==.sig.ed25519' 48 | } 49 | 50 | var stateOOOError = v.appendOOO(v.initial(), hmac_key, missingPrevious) 51 | t.equal(stateOOOError.validated, 0) 52 | t.equal(stateOOOError.error.message, 'message must have keys in allowed order') 53 | 54 | t.end() 55 | }) 56 | -------------------------------------------------------------------------------- /test/data.js: -------------------------------------------------------------------------------- 1 | 2 | var tape = require('tape') 3 | var v = require('../') 4 | 5 | var data = require('ssb-validation-dataset') 6 | 7 | const isObject = (subject) => typeof subject === 'object' && subject != null && Array.isArray(subject) === false 8 | 9 | data.forEach(function (e, i) { 10 | var state = { feeds: {}, queue: [] } 11 | if (isObject(e.message)) { 12 | if (isObject(e.state)) { 13 | e.state.queue = [] 14 | } 15 | state.feeds[e.message.author] = e.state 16 | } 17 | if (e.valid) { 18 | tape(`Message ${i} is valid`, function (t) { 19 | try { 20 | t.equal(v.id(e.message), e.id) 21 | v.append(state, e.hmacKey, e.message) 22 | } catch (err) { 23 | console.log(e) 24 | t.fail(err) 25 | } 26 | t.end() 27 | }) 28 | } else { 29 | tape(`Message ${i} is invalid: ${e.error}`, function (t) { 30 | var state = { feeds: {}, queue: [] } 31 | if (isObject(e.message)) { 32 | state.feeds[e.message.author] = e.state 33 | } 34 | t.throws(function () { 35 | state = v.append(state, e.hmacKey, e.message) 36 | console.log(e) 37 | }) 38 | t.end() 39 | }) 40 | } 41 | }) 42 | -------------------------------------------------------------------------------- /test/error.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var ssbKeys = require('ssb-keys') 3 | var crypto = require('crypto') 4 | 5 | function hash (seed) { 6 | return crypto.createHash('sha256').update(seed).digest() 7 | } 8 | 9 | function id (msg) { 10 | return '%'+crypto.createHash('sha256').update(JSON.stringify(msg, null, 2), 'binary').digest('base64')+'.sha256' 11 | } 12 | 13 | var keys = ssbKeys.generate('ed25519', hash('validation-test-seed1')) 14 | var keys2 = ssbKeys.generate('ed25519', hash('validation-test-seed2')) 15 | 16 | 17 | //generate randomish but deterministic test data. 18 | //this is not intended for security use. 19 | //use a proper key stream (etc) instead. 20 | function pseudorandom (seed, length) { 21 | var a = [] 22 | for(var l = 0; l < length; l += 32) 23 | a.push(hash(''+seed+l)) 24 | return Buffer.concat(a).slice(0, length) 25 | } 26 | 27 | var v = require('..') 28 | 29 | var data = [] 30 | 31 | function test (hmac_key) { 32 | var state = v.initial() 33 | tape('simple', function (t) { 34 | 35 | for(var i = 0; i < 10; i++) { 36 | var msg = v.create(state.feeds[keys.id], keys, hmac_key, {type: 'test', i: i}, +new Date('2017-04-11 8:08 UTC')+i) 37 | 38 | //append sets the state for this author, 39 | //as well as appends the message to the queue. 40 | state = v.append(state, hmac_key, msg) 41 | 42 | } 43 | console.log(state) 44 | t.end() 45 | }) 46 | 47 | tape('rerun', function (t) { 48 | 49 | for(var i = 0; i < state.queue.length; i++) { 50 | try { 51 | console.log(state.queue[i]) 52 | state = v.append(state, hmac_key, state.queue[i]) 53 | t.fail('should have thrown') 54 | } catch (err) { 55 | t.equal(err.fatal, undefined) 56 | } 57 | } 58 | 59 | t.end() 60 | }) 61 | 62 | tape('invalid - fork', function (t) { 63 | 64 | var last = state.queue[9] 65 | var m = v.create({ 66 | id: last.previous, 67 | sequence: 10, 68 | timestamp: last.timestamp, 69 | queue: [] 70 | }, keys, hmac_key, {type: 'invalid'}, last.timestamp+1) 71 | console.log(m) 72 | try { 73 | state = v.append(state, hmac_key, m) 74 | t.fail('should have thrown') 75 | } catch (err) { 76 | console.log(err) 77 | t.equal(err.fatal, true) 78 | t.end() 79 | } 80 | 81 | }) 82 | 83 | // monotonic timestamps no longer a requirement 84 | tape('rewind ok', function (t) { 85 | var last = state.queue[9] 86 | var m = v.create({ 87 | id: state.feeds[keys.id].id, 88 | sequence: 10, 89 | timestamp: last.timestamp-2, queue: [] 90 | }, keys, hmac_key, {type: 'invalid'}, last.timestamp-1) 91 | console.log(m, last) 92 | 93 | state = v.append(state, hmac_key, m) 94 | t.end() 95 | }) 96 | 97 | tape('create with invalid date', function (t) { 98 | t.throws(function () { 99 | v.create(null, keys, hmac_key, {type: 'invalid'}, new Date('foo')) 100 | }) 101 | t.end() 102 | }) 103 | 104 | tape('invalid because of empty content', function (t) { 105 | t.throws(function () { 106 | v.create(null, keys, hmac_key, null, Date.now()) 107 | }) 108 | t.end() 109 | }) 110 | 111 | var ctxt = 112 | pseudorandom('test', 1024).toString('base64')+'.box' 113 | 114 | //create a purposefully invalid message (for testing) 115 | // function create_invalid (state, keys, hmac_key, content, timestamp) { 116 | // invalid.push(ssbKeys.signObj(keys, hmac_key, { 117 | // previous: state ? state.id : null, 118 | // sequence: state ? state.sequence + 1 : 1, 119 | // author: keys.id, 120 | // timestamp: +timestamp, 121 | // hash: 'sha256', 122 | // content: content, 123 | // })) 124 | // return v.create(state, keys, hmac_key, null, timestamp) 125 | // } 126 | 127 | function test_invalid(t, state, keys, hmac_key, content, timestamp) { 128 | var msg = ssbKeys.signObj(keys, hmac_key, { 129 | previous: state ? state.id : null, 130 | sequence: state ? state.sequence + 1 : 1, 131 | author: keys.id, 132 | timestamp: +timestamp, 133 | hash: 'sha256', 134 | content: content, 135 | }) 136 | if(!Buffer.isBuffer(content)) 137 | data.push({state: state, msg: msg, cap: hmac_key, valid: false, id: id(msg)}) 138 | t.throws(function () { 139 | console.log(state, v.create(state, keys, hmac_key, content, timestamp)) 140 | }) 141 | t.throws(function () { 142 | var _state = {feeds: {}} 143 | _state.feeds[keys.id] = state 144 | v.append(_state, hmac_key, msg) 145 | }) 146 | } 147 | 148 | //create invalid messages with invalid orders 149 | function test_invalid_msg(t, state, keys, hmac_key, _msg) { 150 | var msg = ssbKeys.signObj(keys, hmac_key, _msg) 151 | if(!state) 152 | state = { 153 | queue: [], 154 | feeds: {}, 155 | validated: 0 156 | } 157 | data.push({state: state, msg: msg, cap: hmac_key, valid: false, id: id(msg)}) 158 | 159 | t.throws(function () { 160 | console.log(v.append(state, hmac_key, msg)) 161 | }) 162 | t.throws(function () { 163 | var _state = {feeds: {}} 164 | _state.feeds[keys.id] = state 165 | v.append(_state, hmac_key, msg) 166 | }) 167 | } 168 | 169 | 170 | function test_valid(t, state, keys, hmac_key, content, timestamp) { 171 | var msg 172 | msg = v.create(state, keys, hmac_key, content, timestamp) 173 | data.push({state: state, msg: msg, cap: hmac_key, valid: true, id: id(msg)}) 174 | var _state = {queue: [], feeds: {}} 175 | _state.feeds[keys.id] = state 176 | v.append(_state, hmac_key, msg) 177 | t.deepEqual(msg.content, content) 178 | t.equal(msg.timestamp, timestamp) 179 | 180 | } 181 | 182 | function test_valid_msg(t, state, keys, hmac_key, _msg) { 183 | var msg = ssbKeys.signObj(keys, hmac_key, _msg) 184 | data.push({state: state, msg: msg, cap: hmac_key, valid: true, id: id(msg)}) 185 | var _state = {queue: [], feeds: {}} 186 | _state.feeds[keys.id] = state 187 | v.append(_state, hmac_key, msg) 188 | } 189 | 190 | tape('various invalid first messages', function (t) { 191 | var date = +new Date('2017-04-11 9:09 UTC') 192 | test_invalid(t, null, keys, hmac_key, null, date) 193 | test_invalid(t, null, keys, hmac_key, date) 194 | test_invalid(t, null, keys, hmac_key, false, date) 195 | test_invalid(t, null, keys, hmac_key, 0, date) 196 | test_invalid(t, null, keys, hmac_key, 100, date) 197 | test_invalid(t, null, keys, hmac_key, [], date) 198 | test_invalid(t, null, keys, hmac_key, new Buffer('hello'), date) 199 | test_invalid(t, null, keys, hmac_key, new Date(date), date) 200 | 201 | test_invalid(t, null, keys, hmac_key, {}, date) 202 | test_invalid(t, null, keys, hmac_key, {tyfe:'not-okay' }, date) 203 | test_invalid(t,null, keys, hmac_key, {tyfe:'not-okay' }, date) 204 | test_invalid(t, null, keys, hmac_key, { 205 | type: //too long! 206 | pseudorandom('test', 100).toString('base64') 207 | }, date) 208 | 209 | //type too long 210 | test_invalid(t, null, keys, hmac_key, {type:keys.id }, date) 211 | // type too short 212 | test_invalid(t, null, keys, hmac_key, {type:'T' }, date) 213 | // type too short 214 | test_invalid(t, null, keys, hmac_key, {type:'TT' }, date) 215 | // content too long 216 | test_invalid(t, 217 | null, keys, hmac_key, 218 | pseudorandom('test', 8*1024).toString('base64')+'.box', 219 | date 220 | ) 221 | 222 | t.end() 223 | }) 224 | 225 | tape('extended invalid first messages', function (t) { 226 | var date = +new Date('2017-04-11 9:09 UTC') 227 | test_invalid_msg(t, null, keys, hmac_key, { 228 | previous: null, 229 | author: keys.id, 230 | sequence: 1, 231 | timestamp: +date, 232 | hash: 'oanteuhnoatehuneotuh', //unsupported hash 233 | content: {type:'invalid'} 234 | }) 235 | test_invalid_msg(t, null, keys, hmac_key, { 236 | previous: null, 237 | author: keys.id, 238 | sequence: 1, 239 | timestamp: +date, 240 | //missing hash 241 | content: {type:'invalid'} 242 | }) 243 | //invalid orders 244 | test_invalid_msg(t, null, keys, hmac_key, { 245 | content: {type:'invalid'}, 246 | hash: 'sha256', 247 | author: keys.id, 248 | timestamp: +date, 249 | sequence: 1, 250 | previous: null, 251 | }) 252 | test_invalid_msg(t, null, keys, hmac_key, { 253 | previous: null, 254 | author: keys.id, 255 | sequence: 1, 256 | timestamp: +date, 257 | content: {type:'invalid'}, 258 | hash: 'sha256', 259 | }) 260 | 261 | t.end() 262 | }) 263 | 264 | tape('disallow extra fields', function (t) { 265 | 266 | var msg = ssbKeys.signObj(keys, hmac_key, { 267 | previous: null, 268 | author: keys.id, 269 | sequence: 1, 270 | timestamp: +new Date('2017-04-11 9:09 UTC'), 271 | hash: 'sha256', 272 | content: {type: 'invalid'}, 273 | extra: 'INVALID' 274 | }) 275 | var signature = msg.signature 276 | delete msg.signature 277 | delete msg.extra 278 | msg.signature = signature 279 | msg.extra = 'INVALID' 280 | var state = { 281 | queue: [], 282 | feeds: {}, 283 | validated: 0 284 | } 285 | data.push({state: state, msg: msg, cap: hmac_key, valid: false, id: id(msg)}) 286 | 287 | t.throws(function () { 288 | console.log(v.append(state, hmac_key, msg)) 289 | }) 290 | t.throws(function () { 291 | var _state = {feeds: {}} 292 | _state.feeds[keys.id] = state 293 | v.append(_state, hmac_key, msg) 294 | }) 295 | t.end() 296 | }) 297 | 298 | tape('valid messages', function (t) { 299 | //type must be 3 chars 300 | test_valid(t, null, keys, hmac_key, {type:'TTT' }, +new Date('2017-04-11 9:09 UTC')) 301 | 302 | //author and sequence fields may come in either order! 303 | //all other fields must be in exact order. 304 | test_valid_msg(t, null, keys, hmac_key, { 305 | previous: null, 306 | 307 | author: keys.id, sequence: 1, 308 | 309 | timestamp: +new Date('2017-04-11 9:09 UTC'), 310 | hash: 'sha256', 311 | content: { type: 'valid' } 312 | }) 313 | 314 | test_valid_msg(t, null, keys, hmac_key, { 315 | previous: null, 316 | 317 | sequence: 1, author: keys.id, 318 | 319 | timestamp: +new Date('2017-04-11 9:09 UTC'), 320 | hash: 'sha256', 321 | content: { type: 'valid' } 322 | }) 323 | 324 | 325 | //type can be msg id 326 | var msg_id = '%'+hash('test_msg_id').toString('base64')+'.sha256' 327 | test_valid(t, null, keys2, hmac_key, {type: msg_id}, +new Date('2017-04-11 9:09 UTC')) 328 | 329 | //content can be encrypted. 330 | test_valid(t, null, keys2, hmac_key, ctxt, +new Date('2017-04-11 9:09 UTC')) 331 | 332 | //content encrypted with future private box version 333 | test_valid(t, null, keys2, hmac_key, ctxt+'2', +new Date('2017-04-11 9:09 UTC')) 334 | 335 | test_valid(t, null, keys, hmac_key, {type: 'okay'}, +new Date('2019-01-01 0:00 UTC')) 336 | 337 | //for backwards compatibilty reasons, we only count the 338 | //javascript string length of a message, so it may actually 339 | //be encoded as longer that 8k, if it uses unicode. 340 | 341 | //so a message with 7000 euro signs is valid, even though 342 | //it is technically longer than we intended. 343 | 344 | //at some point, we'll introduce a binary encoding for 345 | //messages and we'll fix this then. 346 | var text = '' 347 | for(var i = 0; i < 7000; i++) 348 | text += '\u20ac' 349 | t.ok(text.length < 8124) 350 | 351 | console.log(text.length) 352 | console.log(new Buffer(JSON.stringify({text: text}), 'utf8').length) 353 | 354 | t.ok(new Buffer(JSON.stringify({text: text}), 'utf8').length > 8000) 355 | 356 | test_valid(t, null, keys, hmac_key, {type: 'euros', text: text}, +new Date('2019-01-01 0:00 UTC')) 357 | 358 | t.end() 359 | }) 360 | 361 | } 362 | 363 | test() 364 | test(hash('hmac_key').toString('base64')) 365 | test(hash('hmac_key2').toString('base64')) 366 | test(hash('hmac_key3')) 367 | -------------------------------------------------------------------------------- /test/generate.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape') 2 | var ssbKeys = require('ssb-keys') 3 | 4 | var seed = require('crypto').createHash('sha256').update('validation-test-seed').digest() 5 | var seed2 = require('crypto').createHash('sha256').update('validation-test-seed2').digest() 6 | var keys = ssbKeys.generate('ed25519', seed) 7 | var keys2 = ssbKeys.generate('ed25519', seed2) 8 | const crypto = require('crypto') 9 | 10 | var v = require('../') 11 | 12 | function test (hmac_key) { 13 | var state = v.initial() 14 | 15 | tape('simple', function (t) { 16 | 17 | var msg = v.create(null, keys, hmac_key, {type: 'test'}, +new Date('2017-04-11 8:08 UTC')) 18 | t.notOk(v.checkInvalidCheap(null, msg), 'cheap checks are valid') 19 | console.log(hmac_key) 20 | t.notOk(v.checkInvalid(null, hmac_key, msg), 'signature is valid') 21 | 22 | //append sets the state for this author, 23 | //as well as appends the message to the queue. 24 | state = v.append(state, hmac_key, msg) 25 | 26 | var fstate = state.feeds[keys.id] 27 | 28 | t.equal(fstate.id, v.id(msg)) 29 | t.equal(fstate.timestamp, msg.timestamp) 30 | t.equal(fstate.sequence, msg.sequence) 31 | t.deepEqual(fstate.queue, []) 32 | t.deepEqual(state.queue.map(q => q.value), [msg]) 33 | 34 | v.create(null, keys, hmac_key, {type: 'test'}, +new Date('2017-04-11 8:08 UTC')) 35 | t.ok(v.checkInvalidCheap(fstate, msg), 'cheap checks are invalid (on invalid message)') 36 | t.ok(v.checkInvalid(fstate, hmac_key, msg), 'signature is invalid (on invalid message)') 37 | 38 | t.equal(state.feeds[keys.id].id, v.id(msg)) 39 | t.equal(state.queue.length, 1) 40 | 41 | t.equal(state.validated, 1) 42 | t.equal(state.queued, 0) 43 | 44 | //queue appends to a feed, but does not write check the signature 45 | //(because that is quite slow on javascript crypto) 46 | 47 | var msg2 = v.create(fstate, keys, hmac_key, {type: 'test2'}, +new Date('2017-04-11 8:09 UTC')) 48 | 49 | state = v.queue(state, msg2) 50 | 51 | //doesn't update the feed's state properties, except queue 52 | t.equal(fstate.id, v.id(msg)) 53 | t.equal(fstate.timestamp, msg.timestamp) 54 | t.equal(fstate.sequence, msg.sequence) 55 | 56 | t.deepEqual(fstate.queue.map(q => q.value), [msg2]) 57 | t.deepEqual(state.queue.map(q => q.value), [msg]) 58 | 59 | var msg3 = v.create(fstate, keys, hmac_key, {type: 'test2'}, +new Date('2017-04-11 8:10 UTC')) 60 | t.equal(msg3.previous, v.id(msg2)) 61 | 62 | state = v.append(state, hmac_key, msg3) 63 | 64 | t.deepEqual(state.queue.map(q => q.value), [msg, msg2, msg3]) 65 | console.log(state) 66 | t.end() 67 | }) 68 | 69 | 70 | tape('queue the first item', function (t) { 71 | var msg = v.create(null, keys2, hmac_key, {type: 'test'}, +new Date('2017-04-11 9:09 UTC')) 72 | t.equal(state.queued, 0) 73 | t.equal(state.validated, 3) 74 | 75 | state = v.queue(state, msg) 76 | var fstate = state.feeds[keys2.id] 77 | t.equal(fstate.id, null) 78 | t.equal(fstate.timestamp, null) 79 | t.equal(fstate.sequence, null) 80 | t.deepEqual(fstate.queue.map(q => q.value), [msg]) 81 | 82 | t.equal(state.queued, 1) 83 | 84 | var msg2 = v.create(fstate, keys2, hmac_key, {type: 'test'}, +new Date('2017-04-11 9:10 UTC')) 85 | state = v.queue(state, msg2) 86 | t.equal(state.queued, 2) 87 | t.notOk(state.error) 88 | 89 | t.equal(state.validated, 3) 90 | state = v.validate(state, hmac_key, keys2.id) 91 | 92 | t.equal(state.validated, 5) 93 | t.equal(state.queued, 0) 94 | 95 | t.end() 96 | }) 97 | } 98 | 99 | test() 100 | var hmac_key = crypto.randomBytes(32).toString('base64') 101 | test(hmac_key) 102 | 103 | --------------------------------------------------------------------------------