From 0551fac5d54e159fedb4fda3bde81d54c1068138 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Sun, 20 Oct 2019 11:51:12 +0200 Subject: [PATCH 1/4] docs: initial crypto readme --- src/crypto/README.md | 53 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/crypto/README.md diff --git a/src/crypto/README.md b/src/crypto/README.md new file mode 100644 index 000000000..d9bd32e8e --- /dev/null +++ b/src/crypto/README.md @@ -0,0 +1,53 @@ +interface-crypto +================== + +> A test suite you can use to implement a libp2p crypto module. A libp2p crypto module is used to ensure all exchanged data between two peers is encrypted. + +**Modules that implement the interface** + +- [js-libp2p-secio](https://github.com/libp2p/js-libp2p-secio) + +## Using the Test Suite + +TODO: + +## API + +- `Crypto` + - `protocol`: The protocol id of the crypto module. + - `secureInbound`: Secures inbound connections. + - `secureOutbound`: Secures outbound connections. + +### Secure Inbound + +- `const { conn, remotePeer } = await crypto.secureInbound(localPeer, duplex)` + +Secures an inbound [streaming iterable duplex][iterable-duplex] connection. It returns an encrypted [streaming iterable duplex][iterable-duplex], as well as the [PeerId][peer-id] of the remote peer. + +**Parameters** +- `localPeer` is the [PeerId][peer-id] of the receiving peer. +- `duplex` is the [streaming iterable duplex][iterable-duplex] that will be encryption. + +**Return Value** +- `` + - `conn`: An encrypted [streaming iterable duplex][iterable-duplex]. + - `remotePeer`: The [PeerId][peer-id] of the remote peer. + +### Secure Outbound + +- `const { conn, remotePeer } = await crypto.secureOutbound(localPeer, duplex, remotePeer)` + +Secures an outbound [streaming iterable duplex][iterable-duplex] connection. It returns an encrypted [streaming iterable duplex][iterable-duplex], as well as the [PeerId][peer-id] of the remote peer. + +**Parameters** +- `localPeer` is the [PeerId][peer-id] of the receiving peer. +- `duplex` is the [streaming iterable duplex][iterable-duplex] that will be encryption. +- `remotePeer` is the [PeerId][peer-id] of the remote peer. If provided, implementations **should** use this to validate the integrity of the remote peer. + +**Return Value** +- `` + - `conn`: An encrypted [streaming iterable duplex][iterable-duplex]. + - `remotePeer`: The [PeerId][peer-id] of the remote peer. + +[peer-id]: https://github.com/libp2p/js-peer-id +[iterable-duplex]: https://gist.github.com/alanshaw/591dc7dd54e4f99338a347ef568d6ee9#duplex-it From 11f964c840dc24739d04227dc0fe668066eaaa2d Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Sun, 20 Oct 2019 13:48:23 +0200 Subject: [PATCH 2/4] feat: add basic crypto interface test suite --- package.json | 1 + src/crypto/README.md | 17 +++++- src/crypto/tests/index.js | 86 ++++++++++++++++++++++++++++++ {test => src}/utils/peers.js | 0 test/connection/compliance.spec.js | 2 +- test/crypto/compliance.spec.js | 13 +++++ test/crypto/mock-crypto.js | 69 ++++++++++++++++++++++++ 7 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 src/crypto/tests/index.js rename {test => src}/utils/peers.js (100%) create mode 100644 test/crypto/compliance.spec.js create mode 100644 test/crypto/mock-crypto.js diff --git a/package.json b/package.json index 6277a9c4d..1c1cd43d1 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ }, "devDependencies": { "aegir": "^20.4.1", + "it-handshake": "^1.0.0", "it-pair": "^1.0.0", "it-pipe": "^1.0.1", "peer-info": "^0.17.0" diff --git a/src/crypto/README.md b/src/crypto/README.md index d9bd32e8e..d171dd246 100644 --- a/src/crypto/README.md +++ b/src/crypto/README.md @@ -9,7 +9,22 @@ interface-crypto ## Using the Test Suite -TODO: +You can also check out the [internal test suite](../../test/crypto/compliance.spec.js) to see the setup in action. + +```js +const tests = require('libp2p-interfaces/src/crypto/tests') +const yourCrypto = require('./your-crypto') + +tests({ + setup () { + // Set up your crypto if needed, then return it + return yourCrypto + }, + teardown () { + // Clean up your crypto if needed + } +}) +``` ## API diff --git a/src/crypto/tests/index.js b/src/crypto/tests/index.js new file mode 100644 index 000000000..60ca5395c --- /dev/null +++ b/src/crypto/tests/index.js @@ -0,0 +1,86 @@ +/* eslint-env mocha */ +'use strict' + +const duplexPair = require('it-pair/duplex') +const pipe = require('it-pipe') +const peers = require('../../utils/peers') +const PeerId = require('peer-id') +const { collect } = require('streaming-iterables') +const chai = require('chai') +const expect = chai.expect +chai.use(require('dirty-chai')) + +module.exports = (common) => { + describe('interface-crypto', () => { + let crypto + let localPeer + let remotePeer + + before(async () => { + [ + crypto, + localPeer, + remotePeer + ] = await Promise.all([ + common.setup(), + PeerId.createFromJSON(peers[0]), + PeerId.createFromJSON(peers[1]) + ]) + }) + + after(() => common.teardown && common.teardown()) + + it('has a protocol string', () => { + expect(crypto.protocol).to.exist() + expect(crypto.protocol).to.be.a('string') + }) + + it('it wraps the provided duplex connection', async () => { + const [localConn, remoteConn] = duplexPair() + + const [ + inboundResult, + outboundResult + ] = await Promise.all([ + crypto.secureInbound(remotePeer, localConn), + crypto.secureOutbound(localPeer, remoteConn, remotePeer) + ]) + + // Echo server + pipe(inboundResult.conn, inboundResult.conn) + + // Send some data and collect the result + const input = Buffer.from('data to encrypt') + const result = await pipe( + [input], + outboundResult.conn, + // Convert BufferList to Buffer via slice + (source) => (async function * toBuffer () { + for await (const chunk of source) { + yield chunk.slice() + } + })(), + collect + ) + + expect(result).to.eql([input]) + }) + + it('should return the remote peer id', async () => { + const [localConn, remoteConn] = duplexPair() + + const [ + inboundResult, + outboundResult + ] = await Promise.all([ + crypto.secureInbound(remotePeer, localConn), + crypto.secureOutbound(localPeer, remoteConn, remotePeer) + ]) + + // Inbound should return the initiator (local) peer + expect(inboundResult.remotePeer.id).to.eql(localPeer.id) + // Outbound should return the receiver (remote) peer + expect(outboundResult.remotePeer.id).to.eql(remotePeer.id) + }) + }) +} diff --git a/test/utils/peers.js b/src/utils/peers.js similarity index 100% rename from test/utils/peers.js rename to src/utils/peers.js diff --git a/test/connection/compliance.spec.js b/test/connection/compliance.spec.js index fd4f994d1..d12a87158 100644 --- a/test/connection/compliance.spec.js +++ b/test/connection/compliance.spec.js @@ -3,7 +3,7 @@ const tests = require('../../src/connection/tests') const { Connection } = require('../../src/connection') -const peers = require('../utils/peers') +const peers = require('../../src/utils/peers') const PeerId = require('peer-id') const multiaddr = require('multiaddr') const pair = require('it-pair') diff --git a/test/crypto/compliance.spec.js b/test/crypto/compliance.spec.js new file mode 100644 index 000000000..e050e9761 --- /dev/null +++ b/test/crypto/compliance.spec.js @@ -0,0 +1,13 @@ +/* eslint-env mocha */ +'use strict' + +const tests = require('../../src/crypto/tests') +const mockCrypto = require('./mock-crypto') + +describe('compliance tests', () => { + tests({ + setup () { + return mockCrypto + } + }) +}) diff --git a/test/crypto/mock-crypto.js b/test/crypto/mock-crypto.js new file mode 100644 index 000000000..a5a46eb92 --- /dev/null +++ b/test/crypto/mock-crypto.js @@ -0,0 +1,69 @@ +'use strict' + +const PeerId = require('peer-id') +const handshake = require('it-handshake') +const duplexPair = require('it-pair/duplex') +const pipe = require('it-pipe') + +// A basic transform that does nothing to the data +const transform = () => { + return (source) => (async function * () { + for await (const chunk of source) { + yield chunk + } + })() +} + +module.exports = { + protocol: 'insecure', + secureInbound: async (localPeer, duplex) => { + // 1. Perform a basic handshake. + const shake = handshake(duplex) + shake.write(localPeer.id) + const remoteId = await shake.read() + shake.rest() + + // 2. Create your encryption box/unbox wrapper + const wrapper = duplexPair() + const encrypt = transform() // Use transform iterables to modify data + const decrypt = transform() + + pipe( + wrapper[0], // We write to wrapper + encrypt, // The data is encrypted + shake.stream, // It goes to the remote peer + decrypt, // Decrypt the incoming data + wrapper[0] // Pipe to the wrapper + ) + + return { + conn: wrapper[1], + remotePeer: new PeerId(remoteId.slice()) + } + }, + secureOutbound: async (localPeer, duplex, remotePeer) => { + // 1. Perform a basic handshake. + const shake = handshake(duplex) + shake.write(localPeer.id) + const remoteId = await shake.read() + shake.rest() + + // 2. Create your encryption box/unbox wrapper + const wrapper = duplexPair() + const encrypt = transform() + const decrypt = transform() + + pipe( + wrapper[0], // We write to wrapper + encrypt, // The data is encrypted + shake.stream, // It goes to the remote peer + decrypt, // Decrypt the incoming data + wrapper[0] // Pipe to the wrapper + ) + + return { + conn: wrapper[1], + remotePeer: new PeerId(remoteId.slice()) + } + } +} From 9f3082dd0abb12bfb8afcf789b40e37837cb0d57 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Mon, 21 Oct 2019 12:47:58 +0200 Subject: [PATCH 3/4] feat: add optional remotepeer for inbound feat: add errors export --- src/crypto/README.md | 32 ++++++++++++++++++++++++++++++-- src/crypto/errors.js | 16 ++++++++++++++++ src/crypto/tests/index.js | 20 ++++++++++++++++++-- test/crypto/mock-crypto.js | 10 ++++++++-- 4 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 src/crypto/errors.js diff --git a/src/crypto/README.md b/src/crypto/README.md index d171dd246..dd64acc05 100644 --- a/src/crypto/README.md +++ b/src/crypto/README.md @@ -7,6 +7,16 @@ interface-crypto - [js-libp2p-secio](https://github.com/libp2p/js-libp2p-secio) +## Table of Contents +- [interface-crypto](#interface-crypto) + - [Table of Contents](#table-of-contents) + - [Using the Test Suite](#using-the-test-suite) + - [API](#api) + - [Secure Inbound](#secure-inbound) + - [Secure Outbound](#secure-outbound) + - [Crypto Errors](#crypto-errors) + - [Error Types](#error-types) + ## Using the Test Suite You can also check out the [internal test suite](../../test/crypto/compliance.spec.js) to see the setup in action. @@ -35,13 +45,14 @@ tests({ ### Secure Inbound -- `const { conn, remotePeer } = await crypto.secureInbound(localPeer, duplex)` +- `const { conn, remotePeer } = await crypto.secureInbound(localPeer, duplex, [remotePeer])` Secures an inbound [streaming iterable duplex][iterable-duplex] connection. It returns an encrypted [streaming iterable duplex][iterable-duplex], as well as the [PeerId][peer-id] of the remote peer. **Parameters** - `localPeer` is the [PeerId][peer-id] of the receiving peer. - `duplex` is the [streaming iterable duplex][iterable-duplex] that will be encryption. +- `remotePeer` is the optional [PeerId][peer-id] of the initiating peer, if known. This may only exist during transport upgrades. **Return Value** - `` @@ -62,7 +73,24 @@ Secures an outbound [streaming iterable duplex][iterable-duplex] connection. It **Return Value** - `` - `conn`: An encrypted [streaming iterable duplex][iterable-duplex]. - - `remotePeer`: The [PeerId][peer-id] of the remote peer. + - `remotePeer`: The [PeerId][peer-id] of the remote peer. This **should** match the `remotePeer` parameter, and implementations should enforce this. [peer-id]: https://github.com/libp2p/js-peer-id [iterable-duplex]: https://gist.github.com/alanshaw/591dc7dd54e4f99338a347ef568d6ee9#duplex-it + +## Crypto Errors + +Common crypto errors come with the interface, and can be imported directly. All Errors take an optional message. + +```js +const { + UnexpectedPeerError +} = require('libp2p-interfaces/src/crypto/errors') + +const error = new UnexpectedPeerError('a custom error message') +console.log(error.code === UnexpectedPeerError.code) // true +``` + +### Error Types + +- `UnexpectedPeerError` - Should be thrown when the expected peer id does not match the peer id determined via the crypto exchange diff --git a/src/crypto/errors.js b/src/crypto/errors.js new file mode 100644 index 000000000..378448220 --- /dev/null +++ b/src/crypto/errors.js @@ -0,0 +1,16 @@ +'use strict' + +class UnexpectedPeerError extends Error { + constructor (message = 'Unexpected Peer') { + super(message) + this.code = UnexpectedPeerError.code + } + + static get code () { + return 'ERR_UNEXPECTED_PEER' + } +} + +module.exports = { + UnexpectedPeerError +} diff --git a/src/crypto/tests/index.js b/src/crypto/tests/index.js index 60ca5395c..aa858d9ec 100644 --- a/src/crypto/tests/index.js +++ b/src/crypto/tests/index.js @@ -4,6 +4,7 @@ const duplexPair = require('it-pair/duplex') const pipe = require('it-pipe') const peers = require('../../utils/peers') +const { UnexpectedPeerError } = require('../errors') const PeerId = require('peer-id') const { collect } = require('streaming-iterables') const chai = require('chai') @@ -15,16 +16,19 @@ module.exports = (common) => { let crypto let localPeer let remotePeer + let mitmPeer before(async () => { [ crypto, localPeer, - remotePeer + remotePeer, + mitmPeer ] = await Promise.all([ common.setup(), PeerId.createFromJSON(peers[0]), - PeerId.createFromJSON(peers[1]) + PeerId.createFromJSON(peers[1]), + PeerId.createFromJSON(peers[2]) ]) }) @@ -82,5 +86,17 @@ module.exports = (common) => { // Outbound should return the receiver (remote) peer expect(outboundResult.remotePeer.id).to.eql(remotePeer.id) }) + + it('inbound connections should verify peer integrity if known', async () => { + const [localConn, remoteConn] = duplexPair() + + await Promise.all([ + crypto.secureInbound(remotePeer, localConn, mitmPeer), + crypto.secureOutbound(localPeer, remoteConn, remotePeer) + ]).then(expect.fail, (err) => { + expect(err).to.exist() + expect(err).to.have.property('code', UnexpectedPeerError.code) + }) + }) }) } diff --git a/test/crypto/mock-crypto.js b/test/crypto/mock-crypto.js index a5a46eb92..6d2ee8247 100644 --- a/test/crypto/mock-crypto.js +++ b/test/crypto/mock-crypto.js @@ -4,6 +4,7 @@ const PeerId = require('peer-id') const handshake = require('it-handshake') const duplexPair = require('it-pair/duplex') const pipe = require('it-pipe') +const { UnexpectedPeerError } = require('../../src/crypto/errors') // A basic transform that does nothing to the data const transform = () => { @@ -16,13 +17,18 @@ const transform = () => { module.exports = { protocol: 'insecure', - secureInbound: async (localPeer, duplex) => { + secureInbound: async (localPeer, duplex, expectedPeer) => { // 1. Perform a basic handshake. const shake = handshake(duplex) shake.write(localPeer.id) const remoteId = await shake.read() + const remotePeer = new PeerId(remoteId.slice()) shake.rest() + if (expectedPeer && expectedPeer.id !== remotePeer.id) { + throw new UnexpectedPeerError() + } + // 2. Create your encryption box/unbox wrapper const wrapper = duplexPair() const encrypt = transform() // Use transform iterables to modify data @@ -38,7 +44,7 @@ module.exports = { return { conn: wrapper[1], - remotePeer: new PeerId(remoteId.slice()) + remotePeer } }, secureOutbound: async (localPeer, duplex, remotePeer) => { From 5ec6d00285217acb0e672e69f9748f311ec725b5 Mon Sep 17 00:00:00 2001 From: Jacob Heun Date: Mon, 21 Oct 2019 14:37:29 +0200 Subject: [PATCH 4/4] docs(fix): update src/crypto/README.md Co-Authored-By: Vasco Santos --- src/crypto/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/README.md b/src/crypto/README.md index dd64acc05..7d15a5e7a 100644 --- a/src/crypto/README.md +++ b/src/crypto/README.md @@ -67,7 +67,7 @@ Secures an outbound [streaming iterable duplex][iterable-duplex] connection. It **Parameters** - `localPeer` is the [PeerId][peer-id] of the receiving peer. -- `duplex` is the [streaming iterable duplex][iterable-duplex] that will be encryption. +- `duplex` is the [streaming iterable duplex][iterable-duplex] that will be encrypted. - `remotePeer` is the [PeerId][peer-id] of the remote peer. If provided, implementations **should** use this to validate the integrity of the remote peer. **Return Value**