bip150: add dns resolution. comments.

This commit is contained in:
Christopher Jeffrey 2017-01-15 23:44:01 -08:00
parent 1e9b7363c2
commit 491462f551
No known key found for this signature in database
GPG Key ID: 8962AB9DE6666BBD
2 changed files with 313 additions and 31 deletions

View File

@ -18,47 +18,57 @@ var ec = require('../crypto/ec');
var StaticWriter = require('../utils/staticwriter');
var base58 = require('../utils/base58');
var encoding = require('../utils/encoding');
var IP = require('../utils/ip');
var dns = require('./dns');
/**
* Represents a BIP150 input and output stream.
* Represents a BIP150 input/output stream.
* @exports BIP150
* @constructor
* @param {BIP151} bip151
* @param {String} host
* @param {Boolean} outbound
* @param {AuthDB} db
* @param {Buffer} key - Identity key.
* @property {BIP151} bip151
* @property {BIP151Stream} input
* @property {BIP151Stream} output
* @property {String} hostname
* @property {Boolean} outbound
* @property {AuthDB} db
* @property {Buffer} privateKey
* @property {Buffer} publicKey
* @property {Buffer} peerIdentity
* @property {Boolean} challengeReceived
* @property {Boolean} replyReceived
* @property {Boolean} proposeReceived
* @property {Boolean} challengeSent
* @property {Boolean} auth
* @property {Boolean} completed
*/
function BIP150(bip151, hostname, outbound, db, identity) {
function BIP150(bip151, host, outbound, db, key) {
if (!(this instanceof BIP150))
return new BIP150(bip151, hostname, outbound, db, identity);
assert(bip151, 'BIP150 requires BIP151.');
assert(typeof hostname === 'string', 'Hostname required.');
assert(typeof outbound === 'boolean', 'Outbound flag required.');
assert(db instanceof AuthDB, 'Auth DB required.');
assert(Buffer.isBuffer(identity), 'Identity key required.');
return new BIP150(bip151, host, outbound, db, key);
EventEmitter.call(this);
assert(bip151, 'BIP150 requires BIP151.');
assert(typeof host === 'string', 'Hostname required.');
assert(typeof outbound === 'boolean', 'Outbound flag required.');
assert(db instanceof AuthDB, 'Auth DB required.');
assert(Buffer.isBuffer(key), 'Identity key required.');
this.bip151 = bip151;
this.input = bip151.input;
this.output = bip151.output;
this.hostname = hostname; // ip & port
this.db = db;
this.hostname = host;
this.outbound = outbound;
this.db = db;
this.privateKey = key;
this.publicKey = ec.publicKeyCreate(key, true);
this.peerIdentity = null;
if (this.outbound)
this.peerIdentity = this.db.getKnown(this.hostname);
// Identity keypair
this.privateKey = identity;
this.publicKey = ec.publicKeyCreate(identity, true);
this.challengeReceived = false;
this.replyReceived = false;
this.proposeReceived = false;
@ -67,16 +77,44 @@ function BIP150(bip151, hostname, outbound, db, identity) {
this.completed = false;
this.job = null;
this.timeout = null;
this.onAuth = null;
this._init();
}
util.inherits(BIP150, EventEmitter);
/**
* Initialize BIP150.
* @private
*/
BIP150.prototype._init = function _init() {
if (this.outbound)
this.peerIdentity = this.db.getKnown(this.hostname);
};
/**
* Test whether the state should be
* considered authed. This differs
* for inbound vs. outbound.
* @returns {Boolean}
*/
BIP150.prototype.isAuthed = function isAuthed() {
if (this.outbound)
return this.challengeSent && this.challengeReceived;
return this.challengeReceived && this.replyReceived;
};
/**
* Handle a received challenge hash.
* Returns an authreply signature.
* @param {Buffer} hash
* @returns {Buffer}
* @throws on auth failure
*/
BIP150.prototype.challenge = function challenge(hash) {
var type = this.outbound ? 'r' : 'i';
var msg, sig;
@ -104,6 +142,14 @@ BIP150.prototype.challenge = function challenge(hash) {
return ec.fromDER(sig);
};
/**
* Handle a received reply signature.
* Returns an authpropose hash.
* @param {Buffer} data
* @returns {Buffer}
* @throws on auth failure
*/
BIP150.prototype.reply = function reply(data) {
var type = this.outbound ? 'i' : 'r';
var sig, msg, result;
@ -138,6 +184,13 @@ BIP150.prototype.reply = function reply(data) {
return this.hash(this.input.sid, 'p', this.publicKey);
};
/**
* Handle a received propose hash.
* Returns an authchallenge hash.
* @param {Buffer} hash
* @returns {Buffer}
*/
BIP150.prototype.propose = function propose(hash) {
var match;
@ -162,6 +215,13 @@ BIP150.prototype.propose = function propose(hash) {
return this.hash(this.output.sid, 'r', this.peerIdentity);
};
/**
* Create initial authchallenge hash
* for the peer. The peer's identity
* key must be known.
* @returns {AuthChallengePacket}
*/
BIP150.prototype.toChallenge = function toChallenge() {
var msg;
@ -177,6 +237,17 @@ BIP150.prototype.toChallenge = function toChallenge() {
return new packets.AuthChallengePacket(msg);
};
/**
* Derive new cipher keys based on
* BIP150 data. This differs from
* the regular key derivation of BIP151.
* @param {Buffer} sid - Sesson ID
* @param {Buffer} key - `k1` or `k2`
* @param {Buffer} req - Requesting Identity Key
* @param {Buffer} res - Response Identity Key
* @returns {Buffer}
*/
BIP150.prototype.rekey = function rekey(sid, key, req, res) {
var seed = new Buffer(130);
sid.copy(seed, 0);
@ -186,6 +257,11 @@ BIP150.prototype.rekey = function rekey(sid, key, req, res) {
return crypto.hash256(seed);
};
/**
* Rekey the BIP151 input stream
* using BIP150-style derivation.
*/
BIP150.prototype.rekeyInput = function rekeyInput() {
var stream = this.input;
var req = this.peerIdentity;
@ -195,6 +271,11 @@ BIP150.prototype.rekeyInput = function rekeyInput() {
stream.rekey(k1, k2);
};
/**
* Rekey the BIP151 output stream
* using BIP150-style derivation.
*/
BIP150.prototype.rekeyOutput = function rekeyOutput() {
var stream = this.output;
var req = this.publicKey;
@ -204,6 +285,14 @@ BIP150.prototype.rekeyOutput = function rekeyOutput() {
stream.rekey(k1, k2);
};
/**
* Create a hash using the session ID.
* @param {Buffer} sid
* @param {String} ch
* @param {Buffer} key
* @returns {Buffer}
*/
BIP150.prototype.hash = function hash(sid, ch, key) {
var data = new Buffer(66);
sid.copy(data, 0);
@ -212,6 +301,16 @@ BIP150.prototype.hash = function hash(sid, ch, key) {
return crypto.hash256(data);
};
/**
* Find an authorized peer in the Auth
* DB based on a proposal hash. Note
* that the hash to find is specific
* to the state of BIP151. This results
* in an O(n) search.
* @param {Buffer} hash
* @returns {Buffer|null}
*/
BIP150.prototype.findAuthorized = function findAuthorized(hash) {
var i, key, msg;
@ -228,13 +327,29 @@ BIP150.prototype.findAuthorized = function findAuthorized(hash) {
}
};
/**
* Destroy the BIP150 stream and
* any current running wait job.
*/
BIP150.prototype.destroy = function destroy() {
if (this.timeout != null) {
clearTimeout(this.timeout);
this.timeout = null;
}
if (this.onAuth) {
this.removeListener('auth', this.onAuth);
this.onAuth = null;
}
};
/**
* Cleanup wait job.
* @private
* @returns {Job}
*/
BIP150.prototype.cleanup = function cleanup(err) {
var job = this.job;
@ -249,19 +364,42 @@ BIP150.prototype.cleanup = function cleanup(err) {
this.timeout = null;
}
if (this.onAuth) {
this.removeListener('auth', this.onAuth);
this.onAuth = null;
}
return job;
};
/**
* Resolve the current wait job.
* @private
* @param {Object} result
*/
BIP150.prototype.resolve = function resolve(result) {
var job = this.cleanup();
job.resolve(result);
};
/**
* Reject the current wait job.
* @private
* @param {Error} err
*/
BIP150.prototype.reject = function reject(err) {
var job = this.cleanup();
job.reject(err);
};
/**
* Wait for handshake to complete.
* @param {Number} timeout
* @returns {Promise}
*/
BIP150.prototype.wait = function wait(timeout) {
var self = this;
return new Promise(function(resolve, reject) {
@ -269,6 +407,14 @@ BIP150.prototype.wait = function wait(timeout) {
});
};
/**
* Wait for handshake to complete.
* @private
* @param {Number} timeout
* @param {Function} resolve
* @param {Function} reject
*/
BIP150.prototype._wait = function wait(timeout, resolve, reject) {
var self = this;
@ -276,23 +422,36 @@ BIP150.prototype._wait = function wait(timeout, resolve, reject) {
this.job = co.job(resolve, reject);
if (this.outbound && !this.peerIdentity)
return this.reject(new Error('No identity for ' + this.hostname + '.'));
if (this.outbound && !this.peerIdentity) {
this.reject(new Error('No identity for ' + this.hostname + '.'));
return;
}
this.timeout = setTimeout(function() {
self.reject(new Error('BIP150 handshake timed out.'));
}, timeout);
this.once('auth', function() {
self.resolve();
});
this.onAuth = this.resolve.bind(this);
this.once('auth', this.onAuth);
};
/**
* Serialize the peer's identity
* key as a BIP150 "address".
* @returns {Base58String}
*/
BIP150.prototype.getAddress = function getAddress() {
assert(this.peerIdentity, 'Cannot serialize address.');
return BIP150.address(this.peerIdentity);
};
/**
* Serialize an identity key as a
* BIP150 "address".
* @returns {Base58String}
*/
BIP150.address = function address(key) {
var bw = new StaticWriter(27);
bw.writeU8(0x0f);
@ -312,16 +471,41 @@ function AuthDB(options) {
if (!(this instanceof AuthDB))
return new AuthDB(options);
this.logger = null;
this.resolve = dns.resolve;
this.proxyServer = null;
this.known = {};
this.authorized = [];
this.dns = [];
this._init(options);
}
/**
* Initialize authdb with options.
* @param {Object} options
*/
AuthDB.prototype._init = function _init(options) {
if (!options)
return;
if (options.logger != null) {
assert(typeof options.logger === 'object');
this.logger = options.logger;
}
if (options.resolve != null) {
assert(typeof options.resolve === 'function');
this.resolve = options.resolve;
}
if (options.proxyServer != null) {
assert(typeof options.proxyServer === 'string');
this.proxyServer = options.proxyServer;
}
if (options.knownPeers != null) {
assert(typeof options.knownPeers === 'object');
this.setKnown(options.knownPeers);
@ -333,19 +517,46 @@ AuthDB.prototype._init = function _init(options) {
}
};
/**
* Add a known peer.
* @param {String} host - Peer Hostname
* @param {Buffer} key - Identity Key
*/
AuthDB.prototype.addKnown = function addKnown(host, key) {
var addr;
assert(typeof host === 'string');
assert(Buffer.isBuffer(key) && key.length === 33,
'Invalid public key for known peer.');
addr = IP.parseHost(host);
// Defer this for resolution.
if (addr.version === -1) {
this.dns.push([addr, key]);
return;
}
this.known[host] = key;
};
/**
* Add an authorized peer.
* @param {Buffer} key - Identity Key
*/
AuthDB.prototype.addAuthorized = function addAuthorized(key) {
assert(Buffer.isBuffer(key) && key.length === 33,
'Invalid public key for authorized peer.');
this.authorized.push(key);
};
/**
* Initialize known peers with a host->key map.
* @param {Object} map
*/
AuthDB.prototype.setKnown = function setKnown(map) {
var keys = Object.keys(map);
var i, host, key;
@ -357,6 +568,11 @@ AuthDB.prototype.setKnown = function setKnown(map) {
}
};
/**
* Initialize authorized peers with a list of keys.
* @param {Buffer[]} keys
*/
AuthDB.prototype.setAuthorized = function setAuthorized(keys) {
var i, key;
@ -366,10 +582,76 @@ AuthDB.prototype.setAuthorized = function setAuthorized(keys) {
}
};
AuthDB.prototype.getKnown = function getKnown(host) {
return this.known[host];
/**
* Get a known peer key by hostname.
* @param {String} hostname
* @returns {Buffer|null}
*/
AuthDB.prototype.getKnown = function getKnown(hostname) {
var known = this.known[hostname];
var addr;
if (known)
return known;
addr = IP.parseHost(hostname);
return this.known[addr.host];
};
/**
* Lookup any dns-based known peers.
* @private
* @returns {Promise}
*/
AuthDB.prototype.discover = co(function* discover() {
var jobs = [];
var i, addr;
for (i = 0; i < this.dns.length; i++) {
addr = this.dns[i];
jobs.push(this.populate(addr[0], addr[1]));
}
this.dns.length = 0;
yield Promise.all(jobs);
});
/**
* Populate known peers with a dns-based host.
* @private
* @param {Object} addr
* @param {Buffer} key
* @returns {Promise}
*/
AuthDB.prototype.populate = co(function* populate(addr, key) {
var i, hosts, host;
assert(addr.version === -1);
if (this.logger)
this.logger.info('Resolving authorized hosts from: %s.', addr.host);
try {
hosts = yield this.resolve(addr.host, this.proxyServer);
} catch (e) {
return;
}
for (i = 0; i < hosts.length; i++) {
host = hosts[i];
if (addr.port !== 0)
host = IP.hostname(host, addr.port);
this.known[host] = key;
}
});
/*
* Expose
*/

View File

@ -11,13 +11,13 @@ describe('BIP150', function() {
var sk = ec.generatePrivateKey();
db.addAuthorized(ec.publicKeyCreate(ck, true));
db.addKnown('server', ec.publicKeyCreate(sk, true));
db.addKnown('127.0.0.2', ec.publicKeyCreate(sk, true));
var client = new BIP151();
var server = new BIP151();
client.bip150 = new BIP150(client, 'server', true, db, ck);
server.bip150 = new BIP150(server, 'client', false, db, sk);
client.bip150 = new BIP150(client, '127.0.0.2', true, db, ck);
server.bip150 = new BIP150(server, '127.0.0.1', false, db, sk);
function payload() {
return new Buffer('deadbeef', 'hex');