Merge pull request #2 from ipfs/baseline-test

Add base line test and fix issues with handshaking
This commit is contained in:
Friedel Ziegelmayer 2016-05-23 13:02:36 +02:00
commit 77709c3320
9 changed files with 148 additions and 71 deletions

View File

@ -28,20 +28,21 @@
"debug": "^2.2.0",
"duplexify": "^3.4.3",
"length-prefixed-stream": "^1.5.0",
"libp2p-crypto": "^0.3.1",
"libp2p-crypto": "^0.4.0",
"multihashing": "^0.2.1",
"node-forge": "^0.6.39",
"peer-id": "^0.6.6",
"protocol-buffers": "^3.1.6",
"readable-stream": "1.1.13",
"run-series": "^1.1.4"
"run-series": "^1.1.4",
"through2": "^2.0.1"
},
"devDependencies": {
"aegir": "^3.0.4",
"bl": "^1.1.2",
"chai": "^3.5.0",
"pre-commit": "^1.1.3",
"through2": "^2.0.1"
"stream-pair": "^1.0.3"
},
"pre-commit": [
"lint",

View File

@ -2,38 +2,36 @@
const through = require('through2')
const lpm = require('length-prefixed-stream')
const forge = require('node-forge')
const createBuffer = forge.util.createBuffer
const toForgeBuffer = require('./support').toForgeBuffer
exports.writer = function etmWriter (insecure, cipher, mac) {
const encode = lpm.encode()
const pt = through(function (chunk, enc, cb) {
cipher.update(createBuffer(chunk.toString('binary')))
cipher.update(toForgeBuffer(chunk))
if (cipher.output.length() > 0) {
const data = new Buffer(cipher.output.getBytes(), 'binary')
mac.update(data)
this.push(Buffer.concat([
data,
new Buffer(mac.digest(), 'binary')
]))
const macBuffer = new Buffer(mac.getMac().getBytes(), 'binary')
this.push(Buffer.concat([data, macBuffer]))
// reset hmac
mac.start()
mac.start(null, null)
}
cb()
})
return insecure.pipe(pt).pipe(encode)
pt.pipe(encode).pipe(insecure)
return pt
}
exports.reader = function etmReader (insecure, decipher, mac) {
const decode = lpm.decode()
const pt = through(function (chunk, enc, cb) {
const l = chunk.length
// TODO: check that this mac.getMac().length() is correct
const macSize = mac.getMac().length()
if (l < macSize) {
@ -44,17 +42,19 @@ exports.reader = function etmReader (insecure, decipher, mac) {
const data = chunk.slice(0, mark)
const macd = chunk.slice(mark)
mac.update(data)
const expected = new Buffer(mac.digest(), 'binary')
// reset hmac
mac.start()
// Clear out any previous data
mac.start(null, null)
mac.update(data)
const expected = new Buffer(mac.getMac().getBytes(), 'binary')
// reset hmac
mac.start(null, null)
if (!macd.equals(expected)) {
return cb(new Error(`MAC Invalid: ${macd} != ${expected}`))
return cb(new Error(`MAC Invalid: ${macd.toString('hex')} != ${expected.toString('hex')}`))
}
// all good, decrypt
decipher.update(data)
decipher.update(toForgeBuffer(data))
if (decipher.output.length() > 0) {
const data = new Buffer(decipher.output.getBytes(), 'binary')
@ -64,5 +64,7 @@ exports.reader = function etmReader (insecure, decipher, mac) {
cb()
})
return insecure.pipe(decode).pipe(pt)
insecure.pipe(decode).pipe(pt)
return pt
}

View File

@ -6,8 +6,8 @@ const fs = require('fs')
const path = require('path')
const protobuf = require('protocol-buffers')
const log = debug('libp2p:secio:handshake')
log.error = debug('libp2p:secio:handshake:error')
const log = debug('libp2p:secio')
log.error = debug('libp2p:secio:error')
const pbm = protobuf(fs.readFileSync(path.join(__dirname, '../secio.proto')))
@ -18,17 +18,18 @@ const support = require('../support')
module.exports = function exchange (session, cb) {
log('2. exchange - start')
let eResult
let genSharedKey
let exchangeOut
try {
eResult = crypto.generateEphemeralKeyPair(session.local.curveT)
const eResult = crypto.generateEphemeralKeyPair(session.local.curveT)
session.local.ephemeralPubKey = eResult.key
genSharedKey = eResult.genSharedKey
exchangeOut = makeExchange(session)
} catch (err) {
return cb(err)
}
session.local.ephemeralPubKey = eResult.key
const genSharedKey = eResult.genSharedKey
const exchangeOut = makeExchange(session)
session.insecureLp.write(exchangeOut)
session.insecureLp.once('data', (chunk) => {
const exchangeIn = pbm.Exchange.decode(chunk)
@ -53,17 +54,16 @@ function makeExchange (session) {
session.proposal.in,
session.local.ephemeralPubKey
])
return pbm.Exchange({
epubkey: session.local.ephemeralPubKey,
signature: session.localKey.sign(selectionOut)
})
const epubkey = session.local.ephemeralPubKey
const signature = session.localKey.sign(selectionOut)
return pbm.Exchange.encode({epubkey, signature})
}
function verify (session, exchangeIn) {
log('2.1. verify')
session.remote.ephemeralPubKey = exchangeIn.epubkey
const selectionIn = Buffer.concat([
session.proposal.in,
session.proposal.out,

View File

@ -2,8 +2,8 @@
const duplexify = require('duplexify')
const debug = require('debug')
const log = debug('libp2p:secio:handshake')
log.error = debug('libp2p:secio:handshake:error')
const log = debug('libp2p:secio')
log.error = debug('libp2p:secio:error')
const etm = require('../etm')
@ -16,7 +16,7 @@ module.exports = function finish (session, cb) {
const r = etm.reader(session.insecure, session.remote.cipher, session.remote.mac)
session.secure = duplexify(w, r)
session.secure.write(session.proposal.in.rand)
session.secure.write(session.proposal.randIn)
// read our nonce back
session.secure.once('data', (nonceOut2) => {

View File

@ -3,8 +3,8 @@
const debug = require('debug')
const series = require('run-series')
const log = debug('libp2p:secio:handshake')
log.error = debug('libp2p:secio:handshake:error')
const log = debug('libp2p:secio')
log.error = debug('libp2p:secio:error')
const propose = require('./propose')
const exchange = require('./exchange')

View File

@ -9,8 +9,8 @@ const PeerId = require('peer-id')
const mh = require('multihashing')
const crypto = require('libp2p-crypto')
const log = debug('libp2p:secio:handshake')
log.error = debug('libp2p:secio:handshake:error')
const log = debug('libp2p:secio')
log.error = debug('libp2p:secio:error')
// nonceSize is the size of our nonces (in bytes)
const nonceSize = 16
@ -23,17 +23,20 @@ const support = require('../support')
module.exports = function propose (session, cb) {
log('1. propose - start')
const nonceOut = forge.random.getBytesSync(nonceSize)
const nonceOut = new Buffer(forge.random.getBytesSync(nonceSize), 'binary')
const proposeOut = makeProposal(session, nonceOut)
session.proposal.out = proposeOut
session.proposal.nonceOut = nonceOut
session.insecureLp.write(proposeOut)
session.insecureLp.once('data', (chunk) => {
const proposeIn = readProposal(chunk)
session.proposal.in = proposeIn
let proposeIn
try {
proposeIn = readProposal(chunk)
session.proposal.in = chunk
session.proposal.randIn = proposeIn.rand
identify(session, proposeIn)
} catch (err) {
return cb(err)
@ -54,12 +57,12 @@ module.exports = function propose (session, cb) {
// Generate and send Hello packet.
// Hello = (rand, PublicKey, Supported)
function makeProposal (session, nonceOut) {
session.local.permanentPubKey = session.localKey.getPublic()
session.local.permanentPubKey = session.localKey.public
const myPubKeyBytes = session.local.permanentPubKey.bytes
return pbm.Propose({
return pbm.Propose.encode({
rand: nonceOut,
pubKey: myPubKeyBytes,
pubkey: myPubKeyBytes,
exchanges: support.exchanges.join(','),
ciphers: support.ciphers.join(','),
hashes: support.hashes.join(',')
@ -67,14 +70,14 @@ function makeProposal (session, nonceOut) {
}
function readProposal (bytes) {
return pbm.Proposal.decode(bytes)
return pbm.Propose.decode(bytes)
}
function identify (session, proposeIn) {
log('1.1 identify')
session.remote.permanentPubKey = crypto.unmarshalPublicKey(proposeIn.pubKey)
session.remotePeer = PeerId.createFromPubKey(session.remote.permanentPubKey)
session.remote.permanentPubKey = crypto.unmarshalPublicKey(proposeIn.pubkey)
session.remotePeer = PeerId.createFromPubKey(proposeIn.pubkey.toString('base64'))
log('1.1 identify - %s - identified remote peer as %s', session.localPeer.toB58String(), session.remotePeer.toB58String())
}
@ -91,7 +94,7 @@ function selection (session, nonceOut, proposeIn) {
}
const remote = {
pubKeyBytes: proposeIn.pubKey,
pubKeyBytes: proposeIn.pubkey,
exchanges: proposeIn.exchanges.split(','),
hashes: proposeIn.hashes.split(','),
ciphers: proposeIn.ciphers.split(','),
@ -113,8 +116,14 @@ function selection (session, nonceOut, proposeIn) {
}
function selectBest (local, remote) {
const oh1 = digest(Buffer.concat(remote.pubKeyBytes, local.nonce))
const oh2 = digest(Buffer.concat(local.pubKeyBytes, remote.nonce))
const oh1 = digest(Buffer.concat([
remote.pubKeyBytes,
local.nonce
]))
const oh2 = digest(Buffer.concat([
local.pubKeyBytes,
remote.nonce
]))
const order = Buffer.compare(oh1, oh2)
if (order === 0) {

View File

@ -14,6 +14,7 @@ exports.SecureSession = class SecureSession {
this.remote = {}
this.proposal = {}
this.insecure = insecure
this.secure = null
const e = lpstream.encode()
const d = lpstream.decode()
this.insecureLp = duplexify(e, d)
@ -39,9 +40,16 @@ exports.SecureSession = class SecureSession {
}
}
secureStream (cb) {
this.handshake((err) => {
if (err) return cb(err)
cb(null, this.secure)
})
}
handshake (cb) {
// TODO: figure out how to best handle the handshake timeout
// TODO: better locking
if (this._handshakeLock) {
return cb(new Error('handshake already in progress'))
}

View File

@ -10,8 +10,7 @@ exports.exchanges = [
exports.ciphers = [
'AES-256',
'AES-128',
'Blowfish'
'AES-128'
]
exports.hashes = [
@ -57,6 +56,10 @@ const hashMap = {
SHA512: forge.md.sha512.create()
}
const toForgeBuffer = exports.toForgeBuffer = (buf) => (
forge.util.createBuffer(buf.toString('binary'))
)
function makeMac (hashType, key) {
const hash = hashMap[hashType]
@ -65,7 +68,7 @@ function makeMac (hashType, key) {
}
const mac = forge.hmac.create()
mac.start(hash, key)
mac.start(hash, toForgeBuffer(key))
return mac
}
@ -73,8 +76,8 @@ function makeCipher (cipherType, iv, key) {
if (cipherType === 'AES-128' || cipherType === 'AES-256') {
// aes in counter (CTR) mode because that is what
// is used in go (cipher.NewCTR)
const cipher = forge.cipher.createCipher('AES-CTR', key)
cipher.start({iv})
const cipher = forge.cipher.createCipher('AES-CTR', toForgeBuffer(key))
cipher.start({iv: toForgeBuffer(iv)})
return cipher
}

View File

@ -5,22 +5,18 @@ const expect = require('chai').expect
const through = require('through2')
const bl = require('bl')
const PeerId = require('peer-id')
const duplexify = require('duplexify')
const crypto = require('libp2p-crypto')
const streamPair = require('stream-pair')
const secio = require('../src')
const SecureSession = require('../src').SecureSession
describe('libp2p-secio', () => {
it('exists', () => {
expect(secio).to.exist
})
describe('insecure length prefixed stream', () => {
it('encodes', (done) => {
const id = PeerId.create({bits: 64})
const key = {}
const pt = through()
const insecure = duplexify(pt, pt)
const s = new secio.SecureSession(id, key, insecure)
const insecure = through()
const s = new SecureSession(id, key, insecure)
// encoded on raw
s.insecure.pipe(bl((err, res) => {
@ -37,9 +33,8 @@ describe('libp2p-secio', () => {
it('decodes', (done) => {
const id = PeerId.create({bits: 64})
const key = {}
const pt = through()
const insecure = duplexify(pt, pt)
const s = new secio.SecureSession(id, key, insecure)
const insecure = through()
const s = new SecureSession(id, key, insecure)
// encoded on raw
s.insecureLp.pipe(bl((err, res) => {
@ -52,5 +47,64 @@ describe('libp2p-secio', () => {
s.insecure.write('\u0005world')
s.insecureLp.end()
})
it('all together now', (done) => {
const pair = streamPair.create()
createSession(pair, (err, local) => {
if (err) throw err
createSession(pair.other, (err, remote) => {
if (err) throw err
remote.session.insecureLp.pipe(bl((err, res) => {
if (err) throw err
expect(res.toString()).to.be.eql('hello world')
done()
}))
local.session.insecureLp.write('hello ')
local.session.insecureLp.write('world')
pair.end()
})
})
})
})
it('upgrades a connection', (done) => {
const pair = streamPair.create()
createSession(pair, (err, local) => {
if (err) throw err
createSession(pair.other, (err, remote) => {
if (err) throw err
local.session.secureStream((err, localSecure) => {
if (err) throw err
localSecure.write('hello world')
})
remote.session.secureStream((err, remoteSecure) => {
if (err) throw err
remoteSecure.once('data', (chunk) => {
expect(chunk.toString()).to.be.eql('hello world')
done()
})
})
})
})
})
})
function createSession (insecure, cb) {
crypto.generateKeyPair('RSA', 2048, (err, key) => {
if (err) return cb(err)
const id = PeerId.createFromPrivKey(key.bytes)
cb(null, {
id,
key,
insecure,
session: new SecureSession(id, key, insecure)
})
})
}