mirror of
https://github.com/fluencelabs/js-libp2p-noise
synced 2025-04-25 15:12:32 +00:00
Merge pull request #5 from NodeFactoryIo/morrigan/verify-signature
Authenticate keys - verify signature
This commit is contained in:
commit
5167df18a5
39
README.md
39
README.md
@ -3,6 +3,10 @@
|
||||
[](https://libp2p.io/)
|
||||

|
||||

|
||||
[](https://travis-ci.com/NodeFactoryIo/js-libp2p-noise)
|
||||

|
||||

|
||||
[](https://discuss.libp2p.io)
|
||||
|
||||
> Noise libp2p handshake for js-libp2p
|
||||
|
||||
@ -10,8 +14,39 @@ This repository contains TypeScript implementation of noise protocol, an encrypt
|
||||
|
||||
## Usage
|
||||
|
||||
TBD
|
||||
When published, package should be imported as: `import { Noise } from 'libp2p-noise'`.
|
||||
|
||||
Example of instantiating noise and passing it to the libp2p config:
|
||||
```
|
||||
const NOISE = new Noise(privateKey);
|
||||
|
||||
const libp2p = new Libp2p({
|
||||
modules: {
|
||||
connEncryption: [NOISE],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Where parameters for Noise constructor are:
|
||||
- *private key* - required parameter (32 bytes libp2p peer private key)
|
||||
- *static Noise key* - (optional) existing private Noise static key
|
||||
- *early data* - (optional) an early data payload to be sent in handshake messages
|
||||
|
||||
|
||||
|
||||
## API
|
||||
|
||||
TBD
|
||||
This module exposes a crypto interface, as defined in the repository [js-interfaces](https://github.com/libp2p/js-interfaces).
|
||||
|
||||
[» API Docs](https://github.com/libp2p/js-interfaces/tree/master/src/crypto#api)
|
||||
|
||||
|
||||
## Contribute
|
||||
|
||||
Feel free to join in. All welcome. Open an issue!
|
||||
|
||||
[](https://github.com/ipfs/community/blob/master/contributing.md)
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
47
package.json
47
package.json
@ -5,6 +5,11 @@
|
||||
"repository": "git@github.com:NodeFactoryIo/js-libp2p-noise.git",
|
||||
"author": "NodeFactory <info@nodefactory.io>",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"libp2p",
|
||||
"noise",
|
||||
"crypto"
|
||||
],
|
||||
"scripts": {
|
||||
"prebuild": "rm -rf lib",
|
||||
"build": "babel src -x .ts -d lib --source-maps",
|
||||
@ -13,27 +18,6 @@
|
||||
"pretest": "yarn check-types",
|
||||
"test": "DEBUG=libp2p:noise mocha -r ./babel-register.js \"test/**/*.test.ts\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.6.4",
|
||||
"@babel/core": "^7.6.4",
|
||||
"@babel/plugin-proposal-async-generator-functions": "^7.7.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.6.2",
|
||||
"@babel/preset-env": "^7.6.3",
|
||||
"@babel/preset-typescript": "^7.6.0",
|
||||
"@babel/register": "^7.6.2",
|
||||
"@babel/runtime": "^7.6.3",
|
||||
"@types/chai": "^4.2.4",
|
||||
"@types/mocha": "^5.2.7",
|
||||
"@typescript-eslint/eslint-plugin": "^2.6.0",
|
||||
"@typescript-eslint/parser": "^2.6.0",
|
||||
"bn.js-typings": "^1.0.1",
|
||||
"chai": "^4.2.0",
|
||||
"eslint": "^6.6.0",
|
||||
"libp2p-crypto": "^0.17.1",
|
||||
"mocha": "^6.2.2",
|
||||
"peer-id": "^0.13.5",
|
||||
"typescript": "^3.6.4"
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
[
|
||||
@ -51,6 +35,25 @@
|
||||
"@babel/plugin-proposal-async-generator-functions"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.6.4",
|
||||
"@babel/core": "^7.6.4",
|
||||
"@babel/plugin-proposal-async-generator-functions": "^7.7.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.6.2",
|
||||
"@babel/preset-env": "^7.6.3",
|
||||
"@babel/preset-typescript": "^7.6.0",
|
||||
"@babel/register": "^7.6.2",
|
||||
"@babel/runtime": "^7.6.3",
|
||||
"@types/chai": "^4.2.4",
|
||||
"@types/mocha": "^5.2.7",
|
||||
"@typescript-eslint/eslint-plugin": "^2.6.0",
|
||||
"@typescript-eslint/parser": "^2.6.0",
|
||||
"bn.js-typings": "^1.0.1",
|
||||
"chai": "^4.2.0",
|
||||
"eslint": "^6.6.0",
|
||||
"mocha": "^6.2.2",
|
||||
"typescript": "^3.6.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcrypto": "^4.2.3",
|
||||
"bn.js": "^5.0.0",
|
||||
@ -61,6 +64,8 @@
|
||||
"it-pair": "^1.0.0",
|
||||
"it-pb-rpc": "^0.1.3",
|
||||
"it-pipe": "^1.1.0",
|
||||
"libp2p-crypto": "^0.17.1",
|
||||
"peer-id": "^0.13.5",
|
||||
"protobufjs": "~6.8.8"
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,8 @@ export type PeerId = {
|
||||
pubKey: {
|
||||
marshal(): bytes;
|
||||
};
|
||||
marshalPubKey(): bytes;
|
||||
marshalPrivKey(): bytes;
|
||||
};
|
||||
|
||||
export interface NoiseConnection {
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { Duplex } from "it-pair";
|
||||
import { Handshake } from "./handshake";
|
||||
import { Buffer } from "buffer";
|
||||
|
||||
|
27
src/encoder.ts
Normal file
27
src/encoder.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import {Buffer} from "buffer";
|
||||
import {bytes} from "./@types/basic";
|
||||
import {MessageBuffer} from "./xx";
|
||||
|
||||
export const int16BEEncode = (value, target, offset) => {
|
||||
target = target || Buffer.allocUnsafe(2);
|
||||
return target.writeInt16BE(value, offset);
|
||||
};
|
||||
int16BEEncode.bytes = 2;
|
||||
|
||||
export const int16BEDecode = data => {
|
||||
if (data.length < 2) throw RangeError('Could not decode int16BE');
|
||||
return data.readInt16BE(0);
|
||||
};
|
||||
int16BEDecode.bytes = 2;
|
||||
|
||||
export function encodeMessageBuffer(message: MessageBuffer): bytes {
|
||||
return Buffer.concat([message.ne, message.ns, message.ciphertext]);
|
||||
}
|
||||
|
||||
export function decodeMessageBuffer(message: bytes): MessageBuffer {
|
||||
return {
|
||||
ne: message.slice(0, 32),
|
||||
ns: message.slice(32, 64),
|
||||
ciphertext: message.slice(64, message.length),
|
||||
}
|
||||
}
|
@ -2,15 +2,16 @@ import { Buffer } from "buffer";
|
||||
|
||||
import { bytes, bytes32 } from "./@types/basic";
|
||||
import { NoiseSession, XXHandshake } from "./xx";
|
||||
import { KeyPair } from "./@types/libp2p";
|
||||
import { KeyPair, PeerId } from "./@types/libp2p";
|
||||
import {
|
||||
createHandshakePayload,
|
||||
decodeMessageBuffer,
|
||||
encodeMessageBuffer,
|
||||
getHandshakePayload,
|
||||
logger, signEarlyDataPayload,
|
||||
signEarlyDataPayload,
|
||||
signPayload,
|
||||
verifySignedPayload,
|
||||
} from "./utils";
|
||||
import { logger } from "./logger";
|
||||
import { decodeMessageBuffer, encodeMessageBuffer } from "./encoder";
|
||||
import { WrappedConnection } from "./noise";
|
||||
|
||||
export class Handshake {
|
||||
@ -22,6 +23,7 @@ export class Handshake {
|
||||
private prologue: bytes32;
|
||||
private staticKeys: KeyPair;
|
||||
private connection: WrappedConnection;
|
||||
private remotePeer: PeerId;
|
||||
private xx: XXHandshake;
|
||||
|
||||
constructor(
|
||||
@ -31,6 +33,7 @@ export class Handshake {
|
||||
prologue: bytes32,
|
||||
staticKeys: KeyPair,
|
||||
connection: WrappedConnection,
|
||||
remotePeer: PeerId,
|
||||
handshake?: XXHandshake,
|
||||
) {
|
||||
this.isInitiator = isInitiator;
|
||||
@ -39,32 +42,23 @@ export class Handshake {
|
||||
this.prologue = prologue;
|
||||
this.staticKeys = staticKeys;
|
||||
this.connection = connection;
|
||||
this.remotePeer = remotePeer;
|
||||
|
||||
this.xx = handshake || new XXHandshake();
|
||||
this.session = this.xx.initSession(this.isInitiator, this.prologue, this.staticKeys);
|
||||
}
|
||||
|
||||
// stage 0
|
||||
async propose(earlyData?: bytes): Promise<void> {
|
||||
async propose(): Promise<void> {
|
||||
if (this.isInitiator) {
|
||||
logger("Stage 0 - Initiator starting to send first message.");
|
||||
const signedPayload = signPayload(this.libp2pPrivateKey, getHandshakePayload(this.staticKeys.publicKey));
|
||||
const signedEarlyDataPayload = signEarlyDataPayload(this.libp2pPrivateKey, earlyData || Buffer.alloc(0));
|
||||
const handshakePayload = await createHandshakePayload(
|
||||
this.libp2pPublicKey,
|
||||
this.libp2pPrivateKey,
|
||||
signedPayload,
|
||||
signedEarlyDataPayload
|
||||
);
|
||||
const messageBuffer = await this.xx.sendMessage(this.session, handshakePayload);
|
||||
const messageBuffer = this.xx.sendMessage(this.session, Buffer.alloc(0));
|
||||
this.connection.writeLP(encodeMessageBuffer(messageBuffer));
|
||||
|
||||
logger("Stage 0 - Initiator finished proposing, sent signed NoiseHandshake payload and static public key.");
|
||||
logger("Stage 0 - Initiator finished sending first message.");
|
||||
} else {
|
||||
logger("Stage 0 - Responder waiting to receive first message...");
|
||||
const receivedMessageBuffer = decodeMessageBuffer((await this.connection.readLP()).slice());
|
||||
const plaintext = await this.xx.recvMessage(this.session, receivedMessageBuffer);
|
||||
// TODO: Verify payload
|
||||
this.xx.recvMessage(this.session, receivedMessageBuffer);
|
||||
logger("Stage 0 - Responder received first message.");
|
||||
}
|
||||
}
|
||||
@ -74,36 +68,59 @@ export class Handshake {
|
||||
if (this.isInitiator) {
|
||||
logger('Stage 1 - Initiator waiting to receive first message from responder...');
|
||||
const receivedMessageBuffer = decodeMessageBuffer((await this.connection.readLP()).slice());
|
||||
const plaintext = await this.xx.recvMessage(this.session, receivedMessageBuffer);
|
||||
// TODO: Verify payload
|
||||
const plaintext = this.xx.recvMessage(this.session, receivedMessageBuffer);
|
||||
logger('Stage 1 - Initiator received the message. Got remote\'s static key.');
|
||||
|
||||
logger("Initiator going to check remote's signature...");
|
||||
try {
|
||||
await verifySignedPayload(receivedMessageBuffer.ns, plaintext, this.remotePeer.id);
|
||||
} catch (e) {
|
||||
throw new Error(`Error occurred while verifying signed payload: ${e.message}`);
|
||||
}
|
||||
logger("All good with the signature!");
|
||||
} else {
|
||||
logger('Stage 1 - Responder sending out first message with signed payload and static key.');
|
||||
const signedPayload = signPayload(this.libp2pPrivateKey, getHandshakePayload(this.staticKeys.publicKey));
|
||||
const signedEarlyDataPayload = signEarlyDataPayload(this.libp2pPrivateKey, Buffer.alloc(0));
|
||||
const handshakePayload = await createHandshakePayload(
|
||||
this.libp2pPublicKey,
|
||||
this.libp2pPrivateKey,
|
||||
signedPayload,
|
||||
signedEarlyDataPayload,
|
||||
);
|
||||
|
||||
const messageBuffer = await this.xx.sendMessage(this.session, handshakePayload);
|
||||
const messageBuffer = this.xx.sendMessage(this.session, handshakePayload);
|
||||
this.connection.writeLP(encodeMessageBuffer(messageBuffer));
|
||||
logger('Stage 1 - Responder sent the second handshake message.')
|
||||
logger('Stage 1 - Responder sent the second handshake message with signed payload.')
|
||||
}
|
||||
}
|
||||
|
||||
// stage 2
|
||||
async finish(): Promise<void> {
|
||||
async finish(earlyData?: bytes): Promise<void> {
|
||||
if (this.isInitiator) {
|
||||
logger('Stage 2 - Initiator sending third handshake message.');
|
||||
const messageBuffer = await this.xx.sendMessage(this.session, Buffer.alloc(0));
|
||||
const signedPayload = signPayload(this.libp2pPrivateKey, getHandshakePayload(this.staticKeys.publicKey));
|
||||
const signedEarlyDataPayload = signEarlyDataPayload(this.libp2pPrivateKey, earlyData || Buffer.alloc(0));
|
||||
const handshakePayload = await createHandshakePayload(
|
||||
this.libp2pPublicKey,
|
||||
this.libp2pPrivateKey,
|
||||
signedPayload,
|
||||
signedEarlyDataPayload
|
||||
);
|
||||
const messageBuffer = this.xx.sendMessage(this.session, handshakePayload);
|
||||
this.connection.writeLP(encodeMessageBuffer(messageBuffer));
|
||||
logger('Stage 2 - Initiator sent message.');
|
||||
logger('Stage 2 - Initiator sent message with signed payload.');
|
||||
} else {
|
||||
logger('Stage 2 - Responder waiting for third handshake message...');
|
||||
const receivedMessageBuffer = (await this.connection.readLP()).slice();
|
||||
const plaintext = await this.xx.recvMessage(this.session, decodeMessageBuffer(receivedMessageBuffer));
|
||||
const receivedMessageBuffer = decodeMessageBuffer((await this.connection.readLP()).slice());
|
||||
const plaintext = this.xx.recvMessage(this.session, receivedMessageBuffer);
|
||||
logger('Stage 2 - Responder received the message, finished handshake. Got remote\'s static key.');
|
||||
|
||||
try {
|
||||
await verifySignedPayload(receivedMessageBuffer.ns, plaintext, this.remotePeer.id);
|
||||
} catch (e) {
|
||||
throw new Error(`Error occurred while verifying signed payload: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
2
src/logger.ts
Normal file
2
src/logger.ts
Normal file
@ -0,0 +1,2 @@
|
||||
import debug from "debug";
|
||||
export const logger = debug('libp2p:noise');
|
22
src/noise.ts
22
src/noise.ts
@ -7,7 +7,8 @@ import pipe from 'it-pipe';
|
||||
import lp from 'it-length-prefixed';
|
||||
|
||||
import { Handshake } from "./handshake";
|
||||
import { generateKeypair, int16BEDecode, int16BEEncode } from "./utils";
|
||||
import { generateKeypair } from "./utils";
|
||||
import { int16BEDecode, int16BEEncode } from "./encoder";
|
||||
import { decryptStream, encryptStream } from "./crypto";
|
||||
import { bytes } from "./@types/basic";
|
||||
import { NoiseConnection, PeerId, KeyPair, SecureOutbound } from "./@types/libp2p";
|
||||
@ -46,8 +47,8 @@ export class Noise implements NoiseConnection {
|
||||
*/
|
||||
public async secureOutbound(localPeer: PeerId, connection: any, remotePeer: PeerId): Promise<SecureOutbound> {
|
||||
const wrappedConnection = Wrap(connection);
|
||||
const libp2pPublicKey = localPeer.pubKey.marshal();
|
||||
const handshake = await this.performHandshake(wrappedConnection, true, libp2pPublicKey);
|
||||
const libp2pPublicKey = localPeer.marshalPubKey();
|
||||
const handshake = await this.performHandshake(wrappedConnection, true, libp2pPublicKey, remotePeer);
|
||||
const conn = await this.createSecureConnection(wrappedConnection, handshake);
|
||||
|
||||
return {
|
||||
@ -65,8 +66,8 @@ export class Noise implements NoiseConnection {
|
||||
*/
|
||||
public async secureInbound(localPeer: PeerId, connection: any, remotePeer: PeerId): Promise<SecureOutbound> {
|
||||
const wrappedConnection = Wrap(connection);
|
||||
const libp2pPublicKey = localPeer.pubKey.marshal();
|
||||
const handshake = await this.performHandshake(wrappedConnection, false, libp2pPublicKey);
|
||||
const libp2pPublicKey = localPeer.marshalPubKey();
|
||||
const handshake = await this.performHandshake(wrappedConnection, false, libp2pPublicKey, remotePeer);
|
||||
const conn = await this.createSecureConnection(wrappedConnection, handshake);
|
||||
|
||||
return {
|
||||
@ -79,13 +80,18 @@ export class Noise implements NoiseConnection {
|
||||
connection: WrappedConnection,
|
||||
isInitiator: boolean,
|
||||
libp2pPublicKey: bytes,
|
||||
remotePeer: PeerId,
|
||||
): Promise<Handshake> {
|
||||
const prologue = Buffer.from(this.protocol);
|
||||
const handshake = new Handshake(isInitiator, this.privateKey, libp2pPublicKey, prologue, this.staticKeys, connection);
|
||||
const handshake = new Handshake(isInitiator, this.privateKey, libp2pPublicKey, prologue, this.staticKeys, connection, remotePeer);
|
||||
|
||||
await handshake.propose(this.earlyData);
|
||||
try {
|
||||
await handshake.propose();
|
||||
await handshake.exchange();
|
||||
await handshake.finish();
|
||||
await handshake.finish(this.earlyData);
|
||||
} catch (e) {
|
||||
throw new Error(`Error occurred during handshake: ${e.message}`);
|
||||
}
|
||||
|
||||
return handshake;
|
||||
}
|
||||
|
41
src/utils.ts
41
src/utils.ts
@ -1,13 +1,11 @@
|
||||
import { x25519, ed25519 } from 'bcrypto';
|
||||
import protobuf from "protobufjs";
|
||||
import { Buffer } from "buffer";
|
||||
import debug from "debug";
|
||||
import PeerId from "peer-id";
|
||||
import * as crypto from 'libp2p-crypto';
|
||||
|
||||
import { KeyPair } from "./@types/libp2p";
|
||||
import { bytes } from "./@types/basic";
|
||||
import { MessageBuffer } from "./xx";
|
||||
|
||||
export const logger = debug('libp2p:noise');
|
||||
|
||||
export async function loadPayloadProto () {
|
||||
const payloadProtoBuf = await protobuf.load("protos/payload.proto");
|
||||
@ -70,26 +68,23 @@ export const getHandshakePayload = (publicKey: bytes ) => Buffer.concat([Buffer.
|
||||
|
||||
export const getEarlyDataPayload = (earlyData: bytes) => Buffer.concat([Buffer.from("noise-libp2p-early-data:"), earlyData]);
|
||||
|
||||
export function encodeMessageBuffer(message: MessageBuffer): bytes {
|
||||
return Buffer.concat([message.ne, message.ns, message.ciphertext]);
|
||||
async function isValidPeerId(peerId: bytes, publicKeyProtobuf: bytes) {
|
||||
const generatedPeerId = await PeerId.createFromPubKey(publicKeyProtobuf);
|
||||
return generatedPeerId.id.equals(peerId);
|
||||
}
|
||||
|
||||
export function decodeMessageBuffer(message: bytes): MessageBuffer {
|
||||
return {
|
||||
ne: message.slice(0, 32),
|
||||
ns: message.slice(32, 64),
|
||||
ciphertext: message.slice(64, message.length),
|
||||
export async function verifySignedPayload(noiseStaticKey: bytes, plaintext: bytes, peerId: bytes) {
|
||||
const NoiseHandshakePayload = await loadPayloadProto();
|
||||
const receivedPayload = NoiseHandshakePayload.toObject(NoiseHandshakePayload.decode(plaintext));
|
||||
|
||||
if (!(await isValidPeerId(peerId, receivedPayload.libp2pKey)) ) {
|
||||
throw new Error("Peer ID doesn't match libp2p public key.");
|
||||
}
|
||||
|
||||
const generatedPayload = getHandshakePayload(noiseStaticKey);
|
||||
// Unmarshaling from PublicKey protobuf and taking key buffer only.
|
||||
const publicKey = crypto.keys.unmarshalPublicKey(receivedPayload.libp2pKey).marshal();
|
||||
if (!ed25519.verify(generatedPayload, receivedPayload.noiseStaticKeySignature, publicKey)) {
|
||||
throw new Error("Static key doesn't match to peer that signed payload!");
|
||||
}
|
||||
}
|
||||
|
||||
export const int16BEEncode = (value, target, offset) => {
|
||||
target = target || Buffer.allocUnsafe(2);
|
||||
return target.writeInt16BE(value, offset);
|
||||
};
|
||||
int16BEEncode.bytes = 2;
|
||||
|
||||
export const int16BEDecode = data => {
|
||||
if (data.length < 2) throw RangeError('Could not decode int16BE');
|
||||
return data.readInt16BE(0);
|
||||
};
|
||||
int16BEDecode.bytes = 2;
|
||||
|
62
src/xx.ts
62
src/xx.ts
@ -191,7 +191,7 @@ export class XXHandshake {
|
||||
return SHA256.digest(Buffer.from([...a, ...b]));
|
||||
}
|
||||
|
||||
private async encryptAndHash(ss: SymmetricState, plaintext: bytes): Promise<bytes> {
|
||||
private encryptAndHash(ss: SymmetricState, plaintext: bytes): bytes {
|
||||
let ciphertext;
|
||||
if (this.hasKey(ss.cs)) {
|
||||
ciphertext = this.encryptWithAd(ss.cs, ss.h, plaintext);
|
||||
@ -203,7 +203,7 @@ export class XXHandshake {
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
private async decryptAndHash(ss: SymmetricState, ciphertext: bytes): Promise<bytes> {
|
||||
private decryptAndHash(ss: SymmetricState, ciphertext: bytes): bytes {
|
||||
let plaintext;
|
||||
if (this.hasKey(ss.cs)) {
|
||||
plaintext = this.decryptWithAd(ss.cs, ss.h, ciphertext);
|
||||
@ -223,38 +223,38 @@ export class XXHandshake {
|
||||
return { cs1, cs2 };
|
||||
}
|
||||
|
||||
private async writeMessageA(hs: HandshakeState, payload: bytes): Promise<MessageBuffer> {
|
||||
private writeMessageA(hs: HandshakeState, payload: bytes): MessageBuffer {
|
||||
const ns = Buffer.alloc(0);
|
||||
hs.e = generateKeypair();
|
||||
|
||||
const ne = hs.e.publicKey;
|
||||
|
||||
this.mixHash(hs.ss, ne);
|
||||
const ciphertext = await this.encryptAndHash(hs.ss, payload);
|
||||
const ciphertext = this.encryptAndHash(hs.ss, payload);
|
||||
|
||||
return {ne, ns, ciphertext};
|
||||
}
|
||||
|
||||
private async writeMessageB(hs: HandshakeState, payload: bytes): Promise<MessageBuffer> {
|
||||
private writeMessageB(hs: HandshakeState, payload: bytes): MessageBuffer {
|
||||
hs.e = generateKeypair();
|
||||
const ne = hs.e.publicKey;
|
||||
this.mixHash(hs.ss, ne);
|
||||
|
||||
this.mixKey(hs.ss, this.dh(hs.e.privateKey, hs.re));
|
||||
const spk = Buffer.from(hs.s.publicKey);
|
||||
const ns = await this.encryptAndHash(hs.ss, spk);
|
||||
const ns = this.encryptAndHash(hs.ss, spk);
|
||||
|
||||
this.mixKey(hs.ss, this.dh(hs.s.privateKey, hs.re));
|
||||
const ciphertext = await this.encryptAndHash(hs.ss, payload);
|
||||
const ciphertext = this.encryptAndHash(hs.ss, payload);
|
||||
|
||||
return { ne, ns, ciphertext };
|
||||
}
|
||||
|
||||
private async writeMessageC(hs: HandshakeState, payload: bytes) {
|
||||
private writeMessageC(hs: HandshakeState, payload: bytes) {
|
||||
const spk = Buffer.from(hs.s.publicKey);
|
||||
const ns = await this.encryptAndHash(hs.ss, spk);
|
||||
const ns = this.encryptAndHash(hs.ss, spk);
|
||||
this.mixKey(hs.ss, this.dh(hs.s.privateKey, hs.re));
|
||||
const ciphertext = await this.encryptAndHash(hs.ss, payload);
|
||||
const ciphertext = this.encryptAndHash(hs.ss, payload);
|
||||
const ne = this.createEmptyKey();
|
||||
const messageBuffer: MessageBuffer = {ne, ns, ciphertext};
|
||||
const { cs1, cs2 } = this.split(hs.ss);
|
||||
@ -262,7 +262,7 @@ export class XXHandshake {
|
||||
return { h: hs.ss.h, messageBuffer, cs1, cs2 };
|
||||
}
|
||||
|
||||
private async writeMessageRegular(cs: CipherState, payload: bytes): Promise<MessageBuffer> {
|
||||
private writeMessageRegular(cs: CipherState, payload: bytes): MessageBuffer {
|
||||
const ciphertext = this.encryptWithAd(cs, Buffer.alloc(0), payload);
|
||||
const ne = this.createEmptyKey();
|
||||
const ns = Buffer.alloc(0);
|
||||
@ -270,16 +270,16 @@ export class XXHandshake {
|
||||
return { ne, ns, ciphertext };
|
||||
}
|
||||
|
||||
private async readMessageA(hs: HandshakeState, message: MessageBuffer): Promise<bytes> {
|
||||
private readMessageA(hs: HandshakeState, message: MessageBuffer): bytes {
|
||||
if (x25519.publicKeyVerify(message.ne)) {
|
||||
hs.re = message.ne;
|
||||
}
|
||||
|
||||
this.mixHash(hs.ss, hs.re);
|
||||
return await this.decryptAndHash(hs.ss, message.ciphertext);
|
||||
return this.decryptAndHash(hs.ss, message.ciphertext);
|
||||
}
|
||||
|
||||
private async readMessageB(hs: HandshakeState, message: MessageBuffer): Promise<bytes> {
|
||||
private readMessageB(hs: HandshakeState, message: MessageBuffer): bytes {
|
||||
if (x25519.publicKeyVerify(message.ne)) {
|
||||
hs.re = message.ne;
|
||||
}
|
||||
@ -289,16 +289,16 @@ export class XXHandshake {
|
||||
throw new Error("Handshake state `e` param is missing.");
|
||||
}
|
||||
this.mixKey(hs.ss, this.dh(hs.e.privateKey, hs.re));
|
||||
const ns = await this.decryptAndHash(hs.ss, message.ns);
|
||||
const ns = this.decryptAndHash(hs.ss, message.ns);
|
||||
if (ns.length === 32 && x25519.publicKeyVerify(message.ns)) {
|
||||
hs.rs = ns;
|
||||
}
|
||||
this.mixKey(hs.ss, this.dh(hs.e.privateKey, hs.rs));
|
||||
return await this.decryptAndHash(hs.ss, message.ciphertext);
|
||||
return this.decryptAndHash(hs.ss, message.ciphertext);
|
||||
}
|
||||
|
||||
private async readMessageC(hs: HandshakeState, message: MessageBuffer) {
|
||||
const ns = await this.decryptAndHash(hs.ss, message.ns);
|
||||
private readMessageC(hs: HandshakeState, message: MessageBuffer) {
|
||||
const ns = this.decryptAndHash(hs.ss, message.ns);
|
||||
if (ns.length === 32 && x25519.publicKeyVerify(message.ns)) {
|
||||
hs.rs = ns;
|
||||
}
|
||||
@ -308,7 +308,7 @@ export class XXHandshake {
|
||||
}
|
||||
this.mixKey(hs.ss, this.dh(hs.e.privateKey, hs.rs));
|
||||
|
||||
const plaintext = await this.decryptAndHash(hs.ss, message.ciphertext);
|
||||
const plaintext = this.decryptAndHash(hs.ss, message.ciphertext);
|
||||
const { cs1, cs2 } = this.split(hs.ss);
|
||||
|
||||
return { h: hs.ss.h, plaintext, cs1, cs2 };
|
||||
@ -336,14 +336,14 @@ export class XXHandshake {
|
||||
};
|
||||
}
|
||||
|
||||
public async sendMessage(session: NoiseSession, message: bytes): Promise<MessageBuffer> {
|
||||
public sendMessage(session: NoiseSession, message: bytes): MessageBuffer {
|
||||
let messageBuffer: MessageBuffer;
|
||||
if (session.mc.eqn(0)) {
|
||||
messageBuffer = await this.writeMessageA(session.hs, message);
|
||||
messageBuffer = this.writeMessageA(session.hs, message);
|
||||
} else if (session.mc.eqn(1)) {
|
||||
messageBuffer = await this.writeMessageB(session.hs, message);
|
||||
messageBuffer = this.writeMessageB(session.hs, message);
|
||||
} else if (session.mc.eqn(2)) {
|
||||
const { h, messageBuffer: resultingBuffer, cs1, cs2 } = await this.writeMessageC(session.hs, message);
|
||||
const { h, messageBuffer: resultingBuffer, cs1, cs2 } = this.writeMessageC(session.hs, message);
|
||||
messageBuffer = resultingBuffer;
|
||||
session.h = h;
|
||||
session.cs1 = cs1;
|
||||
@ -354,13 +354,13 @@ export class XXHandshake {
|
||||
throw new Error("CS1 (cipher state) is not defined")
|
||||
}
|
||||
|
||||
messageBuffer = await this.writeMessageRegular(session.cs1, message);
|
||||
messageBuffer = this.writeMessageRegular(session.cs1, message);
|
||||
} else {
|
||||
if (!session.cs2) {
|
||||
throw new Error("CS2 (cipher state) is not defined")
|
||||
}
|
||||
|
||||
messageBuffer = await this.writeMessageRegular(session.cs2, message);
|
||||
messageBuffer = this.writeMessageRegular(session.cs2, message);
|
||||
}
|
||||
} else {
|
||||
throw new Error("Session invalid.")
|
||||
@ -370,14 +370,14 @@ export class XXHandshake {
|
||||
return messageBuffer;
|
||||
}
|
||||
|
||||
public async recvMessage(session: NoiseSession, message: MessageBuffer): Promise<bytes> {
|
||||
public recvMessage(session: NoiseSession, message: MessageBuffer): bytes {
|
||||
let plaintext: bytes;
|
||||
if (session.mc.eqn(0)) {
|
||||
plaintext = await this.readMessageA(session.hs, message);
|
||||
plaintext = this.readMessageA(session.hs, message);
|
||||
} else if (session.mc.eqn(1)) {
|
||||
plaintext = await this.readMessageB(session.hs, message);
|
||||
plaintext = this.readMessageB(session.hs, message);
|
||||
} else if (session.mc.eqn(2)) {
|
||||
const { h, plaintext: resultingPlaintext, cs1, cs2 } = await this.readMessageC(session.hs, message);
|
||||
const { h, plaintext: resultingPlaintext, cs1, cs2 } = this.readMessageC(session.hs, message);
|
||||
plaintext = resultingPlaintext;
|
||||
session.h = h;
|
||||
session.cs1 = cs1;
|
||||
@ -387,12 +387,12 @@ export class XXHandshake {
|
||||
if (!session.cs2) {
|
||||
throw new Error("CS1 (cipher state) is not defined")
|
||||
}
|
||||
plaintext = await this.readMessageRegular(session.cs2, message);
|
||||
plaintext = this.readMessageRegular(session.cs2, message);
|
||||
} else {
|
||||
if (!session.cs1) {
|
||||
throw new Error("CS1 (cipher state) is not defined")
|
||||
}
|
||||
plaintext = await this.readMessageRegular(session.cs1, message);
|
||||
plaintext = this.readMessageRegular(session.cs1, message);
|
||||
}
|
||||
} else {
|
||||
throw new Error("Session invalid.");
|
||||
|
@ -1,15 +1,23 @@
|
||||
import {assert} from "chai";
|
||||
import {assert, expect} from "chai";
|
||||
import Duplex from 'it-pair/duplex';
|
||||
import {Buffer} from "buffer";
|
||||
import Wrap from "it-pb-rpc";
|
||||
|
||||
import {Handshake} from "../src/handshake";
|
||||
import {generateKeypair} from "../src/utils";
|
||||
import {createPeerIds} from "./fixtures/peer";
|
||||
import {createPeerIdsFromFixtures} from "./fixtures/peer";
|
||||
import {getKeyPairFromPeerId} from "./utils";
|
||||
|
||||
|
||||
describe("Handshake", () => {
|
||||
let peerA, peerB, fakePeer;
|
||||
|
||||
before(async () => {
|
||||
[peerA, peerB, fakePeer] = await createPeerIdsFromFixtures(3);
|
||||
});
|
||||
|
||||
it("should propose, exchange and finish handshake", async() => {
|
||||
try {
|
||||
const duplex = Duplex();
|
||||
const connectionFrom = Wrap(duplex[0]);
|
||||
const connectionTo = Wrap(duplex[1]);
|
||||
@ -17,15 +25,12 @@ describe("Handshake", () => {
|
||||
const prologue = Buffer.from('/noise');
|
||||
const staticKeysInitiator = generateKeypair();
|
||||
const staticKeysResponder = generateKeypair();
|
||||
const [peerA, peerB] = await createPeerIds(2);
|
||||
|
||||
const initiatorPrivKey = peerA.privKey.marshal().slice(0, 32);
|
||||
const initiatorPubKey = peerA.pubKey.marshal();
|
||||
const handshakeInitator = new Handshake(true, initiatorPrivKey, initiatorPubKey, prologue, staticKeysInitiator, connectionFrom);
|
||||
const { privateKey: initiatorPrivKey, publicKey: initiatorPubKey } = getKeyPairFromPeerId(peerA);
|
||||
const handshakeInitator = new Handshake(true, initiatorPrivKey, initiatorPubKey, prologue, staticKeysInitiator, connectionFrom, peerB);
|
||||
|
||||
const responderPrivKey = peerB.privKey.marshal().slice(0, 32);
|
||||
const responderPubKey = peerB.pubKey.marshal();
|
||||
const handshakeResponder = new Handshake(false, responderPrivKey, responderPubKey, prologue, staticKeysResponder, connectionTo);
|
||||
const { privateKey: responderPrivKey, publicKey: responderPubKey } = getKeyPairFromPeerId(peerB);
|
||||
const handshakeResponder = new Handshake(false, responderPrivKey, responderPubKey, prologue, staticKeysResponder, connectionTo, peerA);
|
||||
|
||||
await handshakeInitator.propose();
|
||||
await handshakeResponder.propose();
|
||||
@ -51,5 +56,67 @@ describe("Handshake", () => {
|
||||
const encrypted = handshakeInitator.encrypt(Buffer.from("encryptthis"), handshakeInitator.session);
|
||||
const decrypted = handshakeResponder.decrypt(encrypted, handshakeResponder.session);
|
||||
assert(decrypted.equals(Buffer.from("encryptthis")));
|
||||
} catch (e) {
|
||||
assert(false, e.message);
|
||||
}
|
||||
});
|
||||
|
||||
it("Initiator should fail to exchange handshake if given wrong public key in payload", async() => {
|
||||
try {
|
||||
const duplex = Duplex();
|
||||
const connectionFrom = Wrap(duplex[0]);
|
||||
const connectionTo = Wrap(duplex[1]);
|
||||
|
||||
const prologue = Buffer.from('/noise');
|
||||
const staticKeysInitiator = generateKeypair();
|
||||
const staticKeysResponder = generateKeypair();
|
||||
|
||||
const { privateKey: initiatorPrivKey, publicKey: initiatorPubKey } = getKeyPairFromPeerId(peerA);
|
||||
const handshakeInitator = new Handshake(true, initiatorPrivKey, initiatorPubKey, prologue, staticKeysInitiator, connectionFrom, fakePeer);
|
||||
|
||||
const { privateKey: responderPrivKey, publicKey: responderPubKey } = getKeyPairFromPeerId(peerB);
|
||||
const handshakeResponder = new Handshake(false, responderPrivKey, responderPubKey, prologue, staticKeysResponder, connectionTo, peerA);
|
||||
|
||||
await handshakeInitator.propose();
|
||||
await handshakeResponder.propose();
|
||||
|
||||
await handshakeResponder.exchange();
|
||||
await handshakeInitator.exchange();
|
||||
|
||||
assert(false, "Should throw exception");
|
||||
} catch (e) {
|
||||
expect(e.message).equals("Error occurred while verifying signed payload: Peer ID doesn't match libp2p public key.")
|
||||
}
|
||||
});
|
||||
|
||||
it("Responder should fail to exchange handshake if given wrong public key in payload", async() => {
|
||||
try {
|
||||
const duplex = Duplex();
|
||||
const connectionFrom = Wrap(duplex[0]);
|
||||
const connectionTo = Wrap(duplex[1]);
|
||||
|
||||
const prologue = Buffer.from('/noise');
|
||||
const staticKeysInitiator = generateKeypair();
|
||||
const staticKeysResponder = generateKeypair();
|
||||
|
||||
const { privateKey: initiatorPrivKey, publicKey: initiatorPubKey } = getKeyPairFromPeerId(peerA);
|
||||
const handshakeInitator = new Handshake(true, initiatorPrivKey, initiatorPubKey, prologue, staticKeysInitiator, connectionFrom, peerB);
|
||||
|
||||
const { privateKey: responderPrivKey, publicKey: responderPubKey } = getKeyPairFromPeerId(peerB);
|
||||
const handshakeResponder = new Handshake(false, responderPrivKey, responderPubKey, prologue, staticKeysResponder, connectionTo, fakePeer);
|
||||
|
||||
await handshakeInitator.propose();
|
||||
await handshakeResponder.propose();
|
||||
|
||||
await handshakeResponder.exchange();
|
||||
await handshakeInitator.exchange();
|
||||
|
||||
await handshakeInitator.finish();
|
||||
await handshakeResponder.finish();
|
||||
|
||||
assert(false, "Should throw exception");
|
||||
} catch (e) {
|
||||
expect(e.message).equals("Error occurred while verifying signed payload: Peer ID doesn't match libp2p public key.")
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -2,32 +2,31 @@ import { expect, assert } from "chai";
|
||||
import DuplexPair from 'it-pair/duplex';
|
||||
|
||||
import { Noise } from "../src";
|
||||
import { generateEd25519Keys } from "./utils";
|
||||
import {createPeerIds, createPeerIdsFromFixtures} from "./fixtures/peer";
|
||||
import {createPeerIdsFromFixtures} from "./fixtures/peer";
|
||||
import Wrap from "it-pb-rpc";
|
||||
import {Handshake} from "../src/handshake";
|
||||
import {
|
||||
createHandshakePayload,
|
||||
decodeMessageBuffer,
|
||||
encodeMessageBuffer,
|
||||
generateKeypair,
|
||||
getHandshakePayload,
|
||||
signPayload
|
||||
} from "../src/utils";
|
||||
import { decodeMessageBuffer, encodeMessageBuffer } from "../src/encoder";
|
||||
import {XXHandshake} from "../src/xx";
|
||||
import {Buffer} from "buffer";
|
||||
import {getKeyPairFromPeerId} from "./utils";
|
||||
|
||||
describe("Noise", () => {
|
||||
let remotePeer, localPeer;
|
||||
|
||||
before(async () => {
|
||||
[localPeer, remotePeer] = await createPeerIds(2);
|
||||
[localPeer, remotePeer] = await createPeerIdsFromFixtures(2);
|
||||
});
|
||||
|
||||
it("should communicate through encrypted streams", async() => {
|
||||
const libp2pInitPrivKey = localPeer.privKey.marshal().slice(0, 32);
|
||||
const libp2pRespPrivKey = remotePeer.privKey.marshal().slice(0, 32);
|
||||
|
||||
try {
|
||||
const { privateKey: libp2pInitPrivKey } = getKeyPairFromPeerId(localPeer);
|
||||
const { privateKey: libp2pRespPrivKey } = getKeyPairFromPeerId(remotePeer);
|
||||
const noiseInit = new Noise(libp2pInitPrivKey);
|
||||
const noiseResp = new Noise(libp2pRespPrivKey);
|
||||
|
||||
@ -42,11 +41,14 @@ describe("Noise", () => {
|
||||
wrappedOutbound.writeLP(Buffer.from("test"));
|
||||
const response = await wrappedInbound.readLP();
|
||||
expect(response.toString()).equal("test");
|
||||
} catch (e) {
|
||||
assert(false, e.message);
|
||||
}
|
||||
});
|
||||
|
||||
it("should test that secureOutbound is spec compliant", async() => {
|
||||
const libp2pPrivKey = localPeer.privKey.marshal().slice(0, 32);
|
||||
const noiseInit = new Noise(libp2pPrivKey);
|
||||
const { privateKey: libp2pInitPrivKey } = getKeyPairFromPeerId(localPeer);
|
||||
const noiseInit = new Noise(libp2pInitPrivKey);
|
||||
const [inboundConnection, outboundConnection] = DuplexPair();
|
||||
|
||||
const [outbound, { wrapped, handshake }] = await Promise.all([
|
||||
@ -56,28 +58,30 @@ describe("Noise", () => {
|
||||
const prologue = Buffer.from('/noise');
|
||||
const staticKeys = generateKeypair();
|
||||
const xx = new XXHandshake();
|
||||
const libp2pPubKey = remotePeer.pubKey.marshal().slice(32, 64);
|
||||
const handshake = new Handshake(false, libp2pPrivKey, libp2pPubKey, prologue, staticKeys, wrapped, xx);
|
||||
const { privateKey: libp2pPrivKey, publicKey: libp2pPubKey } = getKeyPairFromPeerId(remotePeer);
|
||||
|
||||
const handshake = new Handshake(false, libp2pPrivKey, libp2pPubKey, prologue, staticKeys, wrapped, localPeer, xx);
|
||||
|
||||
let receivedMessageBuffer = decodeMessageBuffer((await wrapped.readLP()).slice());
|
||||
// The first handshake message contains the initiator's ephemeral public key
|
||||
expect(receivedMessageBuffer.ne.length).equal(32);
|
||||
await xx.recvMessage(handshake.session, receivedMessageBuffer);
|
||||
xx.recvMessage(handshake.session, receivedMessageBuffer);
|
||||
|
||||
// Stage 1
|
||||
const signedPayload = signPayload(libp2pPrivKey, getHandshakePayload(staticKeys.publicKey));
|
||||
const handshakePayload = await createHandshakePayload(libp2pPubKey, libp2pPrivKey, signedPayload);
|
||||
|
||||
const messageBuffer = await xx.sendMessage(handshake.session, handshakePayload);
|
||||
const messageBuffer = xx.sendMessage(handshake.session, handshakePayload);
|
||||
wrapped.writeLP(encodeMessageBuffer(messageBuffer));
|
||||
|
||||
// Stage 2 - finish handshake
|
||||
receivedMessageBuffer = decodeMessageBuffer((await wrapped.readLP()).slice());
|
||||
await xx.recvMessage(handshake.session, receivedMessageBuffer);
|
||||
return { wrapped, handshake };
|
||||
xx.recvMessage(handshake.session, receivedMessageBuffer);
|
||||
return {wrapped, handshake};
|
||||
})(),
|
||||
]);
|
||||
|
||||
try {
|
||||
const wrappedOutbound = Wrap(outbound.conn);
|
||||
wrappedOutbound.write(Buffer.from("test"));
|
||||
|
||||
@ -88,5 +92,8 @@ describe("Noise", () => {
|
||||
const decrypted = handshake.decrypt(data, handshake.session);
|
||||
// Decrypted data should match
|
||||
assert(decrypted.equals(Buffer.from("test")));
|
||||
} catch (e) {
|
||||
assert(false, e.message);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
@ -1,5 +1,13 @@
|
||||
import * as crypto from 'libp2p-crypto';
|
||||
import {KeyPair, PeerId} from "../src/@types/libp2p";
|
||||
|
||||
export async function generateEd25519Keys() {
|
||||
return await crypto.keys.generateKeyPair('ed25519');
|
||||
}
|
||||
|
||||
export function getKeyPairFromPeerId(peerId: PeerId): KeyPair {
|
||||
return {
|
||||
privateKey: peerId.privKey.marshal().slice(0, 32),
|
||||
publicKey: peerId.marshalPubKey(),
|
||||
}
|
||||
}
|
||||
|
@ -48,25 +48,26 @@ describe("Index", () => {
|
||||
const respSignedPayload = await libp2pRespKeys.sign(getHandshakePayload(kpResp.publicKey));
|
||||
|
||||
// initiator: new XX noise session
|
||||
const nsInit = await xx.initSession(true, prologue, kpInit);
|
||||
const nsInit = xx.initSession(true, prologue, kpInit);
|
||||
// responder: new XX noise session
|
||||
const nsResp = await xx.initSession(false, prologue, kpResp);
|
||||
const nsResp = xx.initSession(false, prologue, kpResp);
|
||||
|
||||
/* STAGE 0 */
|
||||
|
||||
// initiator creates payload
|
||||
const libp2pInitPrivKey = libp2pInitKeys.marshal().slice(0, 32);
|
||||
const libp2pInitPubKey = libp2pInitKeys.marshal().slice(32, 64);
|
||||
|
||||
const payloadInitEnc = await createHandshakePayload(libp2pInitPubKey, libp2pInitPrivKey, initSignedPayload);
|
||||
|
||||
// initiator sends message
|
||||
const message = Buffer.concat([Buffer.alloc(0), payloadInitEnc]);
|
||||
const messageBuffer = await xx.sendMessage(nsInit, message);
|
||||
const messageBuffer = xx.sendMessage(nsInit, message);
|
||||
|
||||
expect(messageBuffer.ne.length).not.equal(0);
|
||||
|
||||
// responder receives message
|
||||
const plaintext = await xx.recvMessage(nsResp, messageBuffer);
|
||||
const plaintext = xx.recvMessage(nsResp, messageBuffer);
|
||||
console.log("Stage 0 responder payload: ", plaintext);
|
||||
|
||||
/* STAGE 1 */
|
||||
@ -77,22 +78,22 @@ describe("Index", () => {
|
||||
const payloadRespEnc = await createHandshakePayload(libp2pRespPubKey, libp2pRespPrivKey, respSignedPayload);
|
||||
|
||||
const message1 = Buffer.concat([message, payloadRespEnc]);
|
||||
const messageBuffer2 = await xx.sendMessage(nsResp, message1);
|
||||
const messageBuffer2 = xx.sendMessage(nsResp, message1);
|
||||
|
||||
expect(messageBuffer2.ne.length).not.equal(0);
|
||||
expect(messageBuffer2.ns.length).not.equal(0);
|
||||
|
||||
// initiator receive payload
|
||||
const plaintext2 = await xx.recvMessage(nsInit, messageBuffer2);
|
||||
const plaintext2 = xx.recvMessage(nsInit, messageBuffer2);
|
||||
console.log("Stage 1 responder payload: ", plaintext2);
|
||||
|
||||
/* STAGE 2 */
|
||||
|
||||
// initiator send message
|
||||
const messageBuffer3 = await xx.sendMessage(nsInit, Buffer.alloc(0));
|
||||
const messageBuffer3 = xx.sendMessage(nsInit, Buffer.alloc(0));
|
||||
|
||||
// responder receive message
|
||||
const plaintext3 = await xx.recvMessage(nsResp, messageBuffer3);
|
||||
const plaintext3 = xx.recvMessage(nsResp, messageBuffer3);
|
||||
console.log("Stage 2 responder payload: ", plaintext3);
|
||||
|
||||
assert(nsInit.cs1.k.equals(nsResp.cs1.k));
|
||||
|
Loading…
x
Reference in New Issue
Block a user