Skip to content
Browse files

Implement client-side TLS (w/testcases)

  • Loading branch information...
1 parent 45c1fbc commit 0c774b00853dfc73368271afb5a3cac61fb64a0d Niall Smart committed Jan 28, 2012
Showing with 314 additions and 12 deletions.
  1. +1 −0 .gitignore
  2. +10 −3 README.markdown
  3. +57 −9 index.js
  4. +46 −0 test/keys/Makefile
  5. +18 −0 test/keys/ca-key.pem
  6. +20 −0 test/keys/ca.cnf
  7. +15 −0 test/keys/ca.pem
  8. +1 −0 test/keys/ca.srl
  9. +14 −0 test/keys/server-cert.pem
  10. +9 −0 test/keys/server-csr.pem
  11. +9 −0 test/keys/server-key.pem
  12. +19 −0 test/keys/server.cnf
  13. +95 −0 test/tls.js
View
1 .gitignore
@@ -0,0 +1 @@
+node_modules
View
13 README.markdown
@@ -242,16 +242,23 @@ client methods
For all `client` methods, `cb(err, code, lines)` fires with the server response.
-var stream = smtp.connect(host='localhost', port=25, cb)
---------------------------------------------------------
+var stream = smtp.connect(host='localhost', port=25, options={}, cb)
+--------------------------------------------------------------------
Create a new SMTP client connection.
-`host`, `port`, and `cb` are detected by their types in the arguments array so
+`host`, `port`, `options` and `cb` are detected by their types in the arguments array so
they may be in any order.
You can use unix sockets by supplying a string argument that matches `/^[.\/]/`.
+Alternatively supply your own stream as `opts.stream` (the stream must already be connected).
+
+To make a connection using TLS, set `opts.tls` to `true` (for more control you can also assign
+options to pass through to `tls.connect`.) By default, connections to unauthorized servers
+will be closed and the error reported as `cb(error)` (you can provide your own authorization
+logic as `opts.tls.onSecureConnect`).
+
`cb(client)` fires when the connection is ready.
client.helo(domain, cb)
View
66 index.js
@@ -1,4 +1,5 @@
var net = require('net');
+var tls = require('tls');
var proto = exports.protocol = {
client : require('./lib/client/proto'),
@@ -16,26 +17,73 @@ exports.createServer = function (domain, cb) {
});
};
-exports.connect = function (port, host, cb) {
+exports.connect = function (port, host, options, cb) {
var args = [].slice.call(arguments).reduce(function (acc, arg) {
acc[typeof arg] = arg;
return acc;
}, {});
- var cb = args.function;
+
var stream;
+ var tlsOpts;
+
+ cb = args["function"];
+ args.options = args.object || {};
if (args.string && args.string.match(/^[.\/]/)) {
// unix socket
stream = net.createConnection(args.string);
}
else {
- var port = args.number || 25;
- var host = args.string || 'localhost';
- stream = net.createConnection(port, host);
+ port = args.number || 25;
+ host = args.string || 'localhost';
+ tlsOpts = args.options.tls;
+
+ if (tlsOpts) {
+
+ // TODO: is this the right thing to do?
+ var wrapError = function(error) {
+ return error instanceof Error ? error : new Error(error);
+ };
+
+ var onSecureConnect = tlsOpts.onSecureConnect;
+ delete tlsOpts.onSecureConnect;
+
+ stream = tls.connect(
+ port,
+ host,
+ tlsOpts,
+ function() {
+
+ var ok;
+ var result;
+
+ if (onSecureConnect) ok = onSecureConnect(stream);
+ else ok = stream.authorized;
+
+ if (!ok) {
+ stream.end();
+ result = wrapError(stream.authorizationError);
+ }
+ else result = proto.server(stream);
+
+ cb(result);
+ }
+ );
+
+ }
+ else if (args.options.stream) {
+ cb(proto.server(args.options.stream));
+ }
+ else {
+ stream = net.createConnection(port, host);
+
+ stream.on('connect', function () {
+ cb(proto.server(stream));
+ });
+
+ }
+
}
-
- stream.on('connect', function () {
- cb(proto.server(stream));
- });
+
return stream;
};
View
46 test/keys/Makefile
@@ -0,0 +1,46 @@
+#
+# Generate keys for tls.js tests.
+#
+# Derived from node/test/fixtures/keys/Makefile
+#
+
+all: server-cert.pem ca.pem
+
+test: server-verify
+
+clean:
+ rm -f *.pem *.srl *.csr
+
+
+.PHONY: all clean test server-verify
+
+
+#
+# Create Certificate Authority
+#
+ca.pem: ca.cnf
+ openssl req -new -x509 -days 9999 -config ca.cnf -keyout ca-key.pem -out ca.pem
+
+
+#
+# Create server keys
+#
+
+server-key.pem:
+ openssl genrsa -out server-key.pem
+
+server-csr.pem: server.cnf server-key.pem
+ openssl req -new -config server.cnf -key server-key.pem -out server-csr.pem
+
+server-cert.pem: server-csr.pem ca.pem ca-key.pem
+ openssl x509 -req \
+ -days 9999 \
+ -passin "pass:password" \
+ -in server-csr.pem \
+ -CA ca.pem \
+ -CAkey ca-key.pem \
+ -CAcreateserial \
+ -out server-cert.pem
+
+server-verify: ca.pem server-cert.pem
+ openssl verify -CAfile ca.pem server-cert.pem
View
18 test/keys/ca-key.pem
@@ -0,0 +1,18 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: DES-EDE3-CBC,EDC2DBCF49254600
+
+NfBvPLudJSwe23WqrQbOMKrSzlJXVDZ3x0r1jD9JlzvXcHsjn+SP0fx0eQtwljQ4
+JpCU4uKN1kd4Mdy1/UJv5xMyVOAfpt+E+3aRXeW1Tlgoch0rAK+oFyqVJaILZtUK
+Dp4UeuWuDIrgPSB1Yi7SO18Qn97EJqi1l4VQO6cGnCu5p/Z9RVjfcrVGpqTeGwgy
+ulX5vyw2exQcxS9OC5wes8lJy7PKTS0KuVhCOuMau/4U9YgKxi/czM8+J1szGmI+
+AQ84sfUjzb0tc33/Fni/PdAt4xGOAu0dV3glbloaJRgqKLXlVYOmLU5U2wKO7K2P
+A+GCzm2/BZkz1D6yyhE+n2QuR7r50lrj0aWU9ydVpmYHYJbwC53mxBsfsx+hrbvV
+Eo9X6Pf9q7HFqMKaCxEO1CtNS404aacSlkJlSRHz8cR3L31Qj55WzEojorK8Bar/
+BcyHDYIvFqV0M3rbVqcSLX0LVZZLBOrvdeLahA2tCxZpD7rBmnNshpZYPP2rwJkW
+YBcyfRySc0b2frUvt4k0bZYgeGAV0FA3KCxJlIFuh9WHgrC7vGWYdovY+oRGDVG5
+oxSWQKBtHLdwUSSIvPT/BchWffjhPvE7q6/CkpjmI/VtDtBxV6oSppGD/DjEUl6z
+sjyQVM+cqo1t9fHCJTiuMVxPmzBnm+lHMwmsZSZKYB6c4Ltt7N2EuP24E0Qe2RQU
+bwB0BSXIYK+FAVYwG8Sy3FRYUr+Ob5hSeCTTuo6P51fyKZuInTBytqczFVq7/zAv
+xEyyUxg5BfMbxmMfKyPLe/VtgkbpBbZQKCEecnX2PXSyyHvOtZaDaA==
+-----END RSA PRIVATE KEY-----
View
20 test/keys/ca.cnf
@@ -0,0 +1,20 @@
+[ req ]
+default_bits = 1024
+days = 999
+distinguished_name = req_distinguished_name
+attributes = req_attributes
+prompt = no
+output_password = password
+
+[ req_distinguished_name ]
+C = US
+ST = CA
+L = SF
+O = Joyent
+OU = Node.js
+CN = ca1
+emailAddress = ry@tinyclouds.org
+
+[ req_attributes ]
+challengePassword = password
+
View
15 test/keys/ca.pem
@@ -0,0 +1,15 @@
+-----BEGIN CERTIFICATE-----
+MIICazCCAdQCCQD+s8eEvJl2izANBgkqhkiG9w0BAQUFADB6MQswCQYDVQQGEwJV
+UzELMAkGA1UECBMCQ0ExCzAJBgNVBAcTAlNGMQ8wDQYDVQQKEwZKb3llbnQxEDAO
+BgNVBAsTB05vZGUuanMxDDAKBgNVBAMTA2NhMTEgMB4GCSqGSIb3DQEJARYRcnlA
+dGlueWNsb3Vkcy5vcmcwHhcNMTIwMTMwMDIzNjUwWhcNMzkwNjE2MDIzNjUwWjB6
+MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExCzAJBgNVBAcTAlNGMQ8wDQYDVQQK
+EwZKb3llbnQxEDAOBgNVBAsTB05vZGUuanMxDDAKBgNVBAMTA2NhMTEgMB4GCSqG
+SIb3DQEJARYRcnlAdGlueWNsb3Vkcy5vcmcwgZ8wDQYJKoZIhvcNAQEBBQADgY0A
+MIGJAoGBAMSGmtd7g1CNu1RskaPDO9LrreM4VWBa4qYyRRobj1TRNTuWwnKfj4YJ
+qEBGu0nCLIJYnKUFCh+D1N/STcm1ARh6botEb4Q5nEo9IbA+y0imOdP625uka+Pi
+Bk0GvgTCTDkPhdLhv8ORdnhDRIOsPS5J+onXWcOUe6X7li8eG3/LAgMBAAEwDQYJ
+KoZIhvcNAQEFBQADgYEAYylJl2p4xYLOUFDY0pgGo0c9Ls50klBtFgYN7Xfd9KV4
+GqI+kud+gedBseYu6grQ5eUQIcrn8aKpX3FILpvPzG/lDuRYW149tBj89V9LWBbR
+CvYTycvzc3XKOMDeyeN1kXW63Qpv9Dcp6o984W5sURN6dkShRgcHMKc44jaDVeM=
+-----END CERTIFICATE-----
View
1 test/keys/ca.srl
@@ -0,0 +1 @@
+C72215490FF8CA6D
View
14 test/keys/server-cert.pem
@@ -0,0 +1,14 @@
+-----BEGIN CERTIFICATE-----
+MIICIDCCAYkCCQDHIhVJD/jKbTANBgkqhkiG9w0BAQUFADB6MQswCQYDVQQGEwJV
+UzELMAkGA1UECBMCQ0ExCzAJBgNVBAcTAlNGMQ8wDQYDVQQKEwZKb3llbnQxEDAO
+BgNVBAsTB05vZGUuanMxDDAKBgNVBAMTA2NhMTEgMB4GCSqGSIb3DQEJARYRcnlA
+dGlueWNsb3Vkcy5vcmcwHhcNMTIwMTMwMDIzNjUwWhcNMzkwNjE2MDIzNjUwWjBz
+MQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExCzAJBgNVBAcTAlNGMQ8wDQYDVQQK
+EwZKb3llbnQxEDAOBgNVBAsTB05vZGUuanMxDjAMBgNVBAMTBWFnZW50MRcwFQYJ
+KoZIhvcNAQkBFghwYXNzd29yZDBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQDO0KFi
+rxU2N98UuQI6LkRInWbtR3NiGitV7TZhG+hCbk8WrZOyCqwa64KJ5iM/yXq2tMoo
+SvpuvwSaqhNCU7plAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAAbc2oGOkZJsyjjFn
+FyhdDaBSHwGA/zQm/x0F5qgaT0HmkcVB1aCarQ6mYtoTYcrXMxz5rzi3YqT7J2A4
+1FXsAKVDTbynClANLImnpCTeMrw1PUkHMcowCzy7YEofCkjgRfzMXE7IEuc6fv4c
+/EA1ha0uUT+07cGg5mUntqlBzT4=
+-----END CERTIFICATE-----
View
9 test/keys/server-csr.pem
@@ -0,0 +1,9 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIBRjCB8QIBADBzMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExCzAJBgNVBAcT
+AlNGMQ8wDQYDVQQKEwZKb3llbnQxEDAOBgNVBAsTB05vZGUuanMxDjAMBgNVBAMT
+BWFnZW50MRcwFQYJKoZIhvcNAQkBFghwYXNzd29yZDBcMA0GCSqGSIb3DQEBAQUA
+A0sAMEgCQQDO0KFirxU2N98UuQI6LkRInWbtR3NiGitV7TZhG+hCbk8WrZOyCqwa
+64KJ5iM/yXq2tMooSvpuvwSaqhNCU7plAgMBAAGgGTAXBgkqhkiG9w0BCQcxChMI
+cGFzc3dvcmQwDQYJKoZIhvcNAQEFBQADQQBpXsipytwraDnSwfO66MA0LT1bbh/3
+qHBPn0orXnN0v8jbW75L277C103FtNXaS9sZi7CfIwo54SThKSdnuRpb
+-----END CERTIFICATE REQUEST-----
View
9 test/keys/server-key.pem
@@ -0,0 +1,9 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIBOgIBAAJBAM7QoWKvFTY33xS5AjouREidZu1Hc2IaK1XtNmEb6EJuTxatk7IK
+rBrrgonmIz/Jera0yihK+m6/BJqqE0JTumUCAwEAAQJBAM1p8PF4XsQkSEFn5Ktu
+6smI9QM11YeZ4HMPEdTwCAd5iXr8BhdXFjb0VM/Tcjso1aZjVN/6xC7dPAWPY6Dh
+BGECIQDsWASGn658kTgf9RBD11pP0ww2ZSap0PhixL3vpMcUSQIhAOAD6mQQiNlF
+YJuSTDknIRf5tL+Pn4Wo74a3Mo9Jmr09AiBMlhUZXuNXAGP0jyAvK7jbRyOc+Ng3
+jT0AHIsD/hx46QIgFtDvR5/TgGWrkEzXTJ7qpPl+6l/jfIaXVt5D3Qo/I3UCIHle
+9ydA1wLnuA6D6lhU+3rkP6CgPu+7dJ3dcmLo/6EG
+-----END RSA PRIVATE KEY-----
View
19 test/keys/server.cnf
@@ -0,0 +1,19 @@
+[ req ]
+default_bits = 1024
+days = 999
+distinguished_name = req_distinguished_name
+attributes = req_attributes
+prompt = no
+
+[ req_distinguished_name ]
+C = US
+ST = CA
+L = SF
+O = Joyent
+OU = Node.js
+CN = agent
+emailAddress = password
+
+[ req_attributes ]
+challengePassword = password
+
View
95 test/tls.js
@@ -0,0 +1,95 @@
+var smtp = require('../');
+var tls = require('tls');
+var fs = require('fs');
+var test = require('tap').test;
+
+var serverPort = 8000;
+
+var keys = {
+ key: fs.readFileSync('./keys/server-key.pem'),
+ cert: fs.readFileSync('./keys/server-cert.pem'),
+ ca: fs.readFileSync('./keys/ca.pem')
+};
+
+server = tls.createServer({key: keys.key, cert: keys.cert});
+
+server.on('secureConnection', function(s) {
+ s.write("220 localhost ESMTP\r\n");
+ s.on('data', function(data) {
+ s.end("421 Service unavailable\r\n");
+ });
+});
+
+test('TLS - unauthorized', {timeout: 1000}, function(t) {
+
+ t.plan(2);
+
+ var options = {
+ tls: true
+ };
+
+ server.listen(serverPort, function() {
+ smtp.connect(serverPort, options, function(err) {
+ server.close();
+ t.ok(err instanceof Error, "connect should fail");
+ t.equals(err.message, "UNABLE_TO_VERIFY_LEAF_SIGNATURE", "connect should fail");
+ t.end();
+ });
+ });
+});
+
+test('TLS - unauthorized with callback', {timeout: 1000}, function(t) {
+
+ t.plan(4);
+
+ var options = {
+ tls: {
+ onSecureConnect: function(s) {
+ t.ok(!s.authorized);
+ return true;
+ }
+ }
+ };
+
+ server.listen(serverPort, function() {
+ smtp.connect(serverPort, options, function(session) {
+ server.close();
+ t.error(session instanceof Error, "connect should succeed");
+ session.on('greeting', function(code, messages) {
+ t.equal(code, 220);
+ t.equal(messages[0], "localhost ESMTP");
+ t.end();
+ session.quit();
+ })
+ });
+ });
+});
+
+test('TLS - authorized', {timeout: 1000}, function(t) {
+
+ t.plan(4);
+
+ var options = {
+ tls: {
+ ca: [ keys.ca ],
+ onSecureConnect: function(s) {
+ t.ok(s.authorized, "should be authorized");
+ return s.authorized;
+ }
+ }
+ };
+
+ server.listen(serverPort, function() {
+
+ smtp.connect(serverPort, options, function(session) {
+ server.close();
+ t.error(session instanceof Error, "connect should succeed");
+ session.on('greeting', function(code, messages) {
+ t.equal(code, 220);
+ t.equal(messages[0], "localhost ESMTP");
+ t.end();
+ session.quit();
+ })
+ });
+ });
+});

0 comments on commit 0c774b0

Please sign in to comment.
Something went wrong with that request. Please try again.