node-posh
Version:
PKIX Over Secure HTTP (POSH) tools for node.js
402 lines (344 loc) • 12.8 kB
JavaScript
// Generated by CoffeeScript 1.6.3
/*
# node-posh #
See [draft-miller-posh](http://tools.ietf.org/html/draft-miller-posh-00)
for more details on PKIX over Secure HTTP (POSH).
*/
(function() {
var POSH, Q, dns, events, fs, net, pem, request, services, tls, _cert_to_jwk, _cert_to_x5c, _get_cert_info, _get_x5c_info, _hex_to_base64url,
__bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
fs = require('fs');
events = require('events');
dns = require('dns');
net = require('net');
tls = require('tls');
pem = require('pem');
Q = require('q');
request = require('request');
services = require('service-parser');
_hex_to_base64url = function(hex) {
var b64;
if (hex.length % 2) {
hex = '0' + hex;
}
b64 = new Buffer(hex, 'hex').toString('base64');
return b64.replace(/\=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
};
_cert_to_x5c = function(cert, maxdepth) {
if (maxdepth == null) {
maxdepth = 0;
}
/*
Convert a PEM-encoded certificate to the version used in the x5c element
of a [JSON Web Key](http://tools.ietf.org/html/draft-ietf-jose-json-web-key).
* `cert` PEM-encoded certificate chain
* `maxdepth` The maximum number of certificates to use from the chain.
*/
cert = cert.replace(/-----[^\n]+\n?/gm, ',').replace(/\n/g, '');
cert = cert.split(',').filter(function(c) {
return c.length > 0;
});
if (maxdepth > 0) {
cert = cert.splice(0, maxdepth);
}
return cert;
};
_get_cert_info = function(cert) {
return Q.spread([Q.nfcall(pem.getModulus, cert), Q.nfcall(pem.getFingerprint, cert), Q.nfcall(pem.readCertificateInfo, cert)], function(modulus, fingerprint, info) {
info.modulus = _hex_to_base64url(modulus.modulus);
info.fingerprint = _hex_to_base64url(fingerprint.fingerprint.replace(/:/g, ''));
return info;
});
};
_get_x5c_info = function(x5c) {
var cert, y;
cert = ((function() {
var _i, _ref, _results;
_results = [];
for (y = _i = 0, _ref = x5c.length; _i <= _ref; y = _i += 64) {
_results.push(x5c.slice(y, +(y + 63) + 1 || 9e9));
}
return _results;
})()).join('\n');
"-----BEGIN CERTIFICATE-----\n" + c + "\n-----END CERTIFICATE-----";
return _get_cert_info(cert);
};
_cert_to_jwk = function(cert, maxdepth) {
/*
Convert a certificate to a
[JSON Web Key](http://tools.ietf.org/html/draft-ietf-jose-json-web-key)
representation.
* `cert` PEM-encoded certificate chain
* `maxdepth` The maximum number of certificates to use from the chain.
*/
return _get_cert_info(cert).then(function(info) {
return {
kty: "RSA",
kid: "" + info.commonName + ":" + info.fingerprint,
n: info.modulus,
e: "AQAB",
x5c: _cert_to_x5c(cert, maxdepth)
};
});
};
exports.create = function(certs, maxdepth) {
/*
Create a POSH document from a list of certificates.
* `certs` an array of PEM-encoded certificate chains. The first certificate
in each chain will be extracted into the POSH public key information.
* `maxdepth` the maxiumum number of certificates to use from each chain.
* __returns__ a [Q](https://github.com/kriskowal/q) promise that will be
fulfilled with a JavaScript representation (not a JSON string!) of the
POSH document.
*/
var p;
if (!Array.isArray(certs)) {
certs = [certs];
}
if (certs.length === 0) {
throw new Error('No certs specified');
}
p = certs.map(function(c) {
return _cert_to_jwk(c, maxdepth);
});
return Q.all(p).then(function(all) {
return {
keys: all
};
});
};
exports.write = function(dir, service, posh) {
/*
Write a file with the given POSH object in a file with the correct name
for the given service.
* `dir` the directory to write into
* `service` the SRV record name for the target service.
Example: "_xmpp-server._tcp"
* __returns__ a [Q](https://github.com/kriskowal/q) promise that will be
fulfilled when the file is finished writing
*/
return Q.nfcall(fs.writeFile, "" + dir + "/posh." + service + ".json", JSON.stringify(posh));
};
POSH = (function(_super) {
__extends(POSH, _super);
/*
Make a POSH-verified connection to a given domain on a given service.
Events:
* `'posh request', url` about to request a POSH document at the given URL
* `'no posh', er` No POSH document could be retrieved. Not really an error.
* `'connecting', host, port, tls` Connecting on the given host and port. If
`tls` is true, a TLS handshake will start as soon as the connection
finishes.
* `'error', er` an error was detected.
* `'connect', socket` the given socket was connected
* `'secure', service_cert, posh_document` the connection is secure
either by RFC 6125 or POSH. The posh_document is null if the service_cert
was valid via RFC 6125.
* `'insecure', service_cert, posh_document` the connection could not be
determined to be secure. The posh_document is null if it could not be
retrieved.
*/
function POSH(dns_domain, dns_srv, options) {
var k, m, serv, v, _ref;
this.dns_domain = dns_domain;
this.dns_srv = dns_srv;
this._check_cert = __bind(this._check_cert, this);
/*
Create a POSH connection object
* `dns_domain` connect to the given domain
* `dns_srv` the DNS SRV protocol name to connect with.
For example, "_xmpp-server._tcp"
* `options` a configuration object
* `fallback_port` The port to fall back on if SRV fails. If -1, use
the port for the given SRV protocol name from /etc/services. Defaults
to -1.
* `start_tls` Don't do TLS immediately after connecting. Instead, wait
for a listener for the `connect` event to call `start_tls()`.
* `ca` An array of zero or more certificate authority (CA) certs to trust
when making HTTPS calls for POSH certs.
*/
POSH.__super__.constructor.call(this, this);
this.options = {
fallback_port: -1,
start_tls: false,
ca: []
};
_ref = options != null ? options : {};
for (k in _ref) {
v = _ref[k];
this.options[k] = v;
}
if (this.options.fallback_port === -1) {
m = this.dns_srv.match(/^_([^\.]+)/);
if (m) {
serv = services.getByName(m[1]);
if (serv) {
this.options.fallback_port = serv.port;
}
}
}
this.posh_url = "https://" + this.dns_domain + "/.well-known/posh." + this.dns_srv + ".json";
this.host = this.dns_domain;
this.port = this.options.fallback_port;
}
POSH.prototype.get_posh = function() {
/*
Attempt to get the POSH assertion for the domain and SRV protocol
given in the constructor
* __returns__ a [Q](https://github.com/kriskowal/q) promise that will be
fulfilled with the POSH object when/if it is retrieved. Rejections of
this promise usually shouldn't be treated as an error.
*/
var _this = this;
this.emit('posh request', this.posh_url);
return Q.nfcall(request, {
url: this.posh_url,
followRedirect: false,
ca: this.options.ca
}).then(function(resp) {
var er, status;
status = resp[0].statusCode;
if (status !== 200) {
er = new Error("HTTP error " + status);
_this.emit('no posh', er);
return Q.reject(er);
} else {
return _this.posh_json = JSON.parse(resp[1]);
}
}, function(er) {
_this.emit('no posh', er);
return Q.reject(new Error('No POSH HTTP server'));
});
};
POSH.prototype.resolve = function() {
/*
Do the SRV resolution.
* __returns__ a [Q](https://github.com/kriskowal/q) promise that will be
fulfilled with `host`, `port` when complete. Ignores DNS errors, returning
the original domain and fallback port.
*/
var _this = this;
return Q.nfcall(dns.resolveSrv, "" + this.dns_srv + "." + this.dns_domain).then(function(addresses) {
var _ref;
if (addresses.length) {
_ref = addresses[0], _this.host = _ref.name, _this.port = _ref.port;
}
return [_this.host, _this.port];
}, function(er) {
return [this.host, this.port];
});
};
POSH.prototype._connect_internal = function(tls, connector) {
var _this = this;
this.posh = this.get_posh();
return this.resolve().spread(function(host, port) {
var d;
_this.emit('connecting', host, port, tls);
d = Q.defer();
_this.cli = connector(host, port);
_this.cli.on('error', function(er) {
_this.emit('error', er);
return d.reject(er);
});
_this.cli.once('connect', function() {
_this.emit('connect', _this.cli);
return d.resolve(_this.cli);
});
return d.promise;
});
};
POSH.prototype.connect_plain = function() {
/*
Connect without starting TLS. Wait for the `connect` event, then call
`start_tls`.
* __returns__ a [Q](https://github.com/kriskowal/q) promise that will be
fulfilled with the connected socket.
*/
return this._connect_internal(false, function(host, port) {
return net.connect({
host: host,
port: port
});
});
};
POSH.prototype._check_cert = function() {
var cert, d,
_this = this;
cert = this.cli.getPeerCertificate();
if (this.cli.authorized) {
this.emit('secure', cert);
return Q.resolve(true, cert);
} else {
d = Q.defer();
this.posh.then(function(pjson) {
var exp, k, modu, _i, _len, _ref;
if (pjson != null) {
modu = _hex_to_base64url(cert.modulus);
exp = _hex_to_base64url(cert.exponent);
_ref = pjson.keys;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
k = _ref[_i];
if ((k.n === modu) && (k.e === exp)) {
_this.emit('secure', cert, pjson);
return d.resolve(true, cert, pjson);
}
}
}
_this.emit('insecure', cert, pjson);
return d.resolve(false, cert, pjson);
}, function(er) {
_this.emit('insecure', cert);
return d.resolve(false, cert);
});
return d.promise;
}
};
POSH.prototype.connect_tls = function() {
/*
Connect to the given serice, and start TLS immediately.
* __returns__ a [Q](https://github.com/kriskowal/q) promise that will be
fulfilled with the connected socket.
*/
return this._connect_internal(true, function(host, port) {
return tls.connect({
host: this.host,
port: this.port,
rejectUnauthorized: false
});
}).then(this._check_cert);
};
POSH.prototype.start_tls = function() {
/*
On the already-connected socket, start a TLS handshake. This MUST occur
after the 'connect' event has been called.
*/
var _this = this;
this.cli = tls.connect({
socket: this.cli,
rejectUnauthorized: false,
servername: this.dns_domain
}, this._check_cert);
return this.cli.on('error', function(er) {
return _this.emit('error', er);
});
};
POSH.prototype.connect = function() {
/*
Connect to the domain on the specified service, using either an initially-
plaintext approach (options.start_tls=true), or an initially-encrypted
approach (options.start_tls=false).
* __returns__ a [Q](https://github.com/kriskowal/q) promise that will be
fulfilled with the connected socket.
*/
if (this.options.start_tls) {
return this.connect_plain();
} else {
return this.connect_tls();
}
};
return POSH;
})(events.EventEmitter);
exports.POSH = POSH;
}).call(this);