UNPKG

node-posh

Version:

PKIX Over Secure HTTP (POSH) tools for node.js

402 lines (344 loc) 12.8 kB
// 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);