{"action":"created","comment":{"url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/pulls/comments/24347514","id":24347514,"diff_hunk":"@@ -0,0 +1,564 @@\n+#!/usr/bin/env python\n+\n+\"\"\"Tests for standalone_authenticator.py.\"\"\"\n+\n+import unittest\n+import mock\n+import pkg_resources\n+from letsencrypt.client.challenge_util import DvsniChall\n+\n+\n+# Classes based on to allow interrupting infinite loop under test\n+# after one iteration, based on.\n+# http://igorsobreira.com/2013/03/17/testing-infinite-loops.html\n+\n+class SocketAcceptOnlyNTimes(object):\n+    # pylint: disable=too-few-public-methods\n+    \"\"\"\n+    Callable that will raise `CallableExhausted`\n+    exception after `limit` calls, modified to also return\n+    a tuple simulating the return values of a socket.accept()\n+    call\n+    \"\"\"\n+    def __init__(self, limit):\n+        self.limit = limit\n+        self.calls = 0\n+\n+    def __call__(self):\n+        self.calls += 1\n+        if self.calls > self.limit:\n+            raise CallableExhausted\n+        # Modified here for a single use as socket.accept()\n+        return (mock.MagicMock(), \"ignored\")\n+\n+class CallableExhausted(Exception):\n+    # pylint: disable=too-few-public-methods\n+    \"\"\"Exception raised when a method is called more than the\n+    specified number of times.\"\"\"\n+    pass\n+\n+\n+class PackAndUnpackTests(unittest.TestCase):\n+    \"\"\"Tests for byte packing and unpacking routines used for TLS\n+    parsing.\"\"\"\n+    def test_pack_and_unpack_bytes(self):\n+        from letsencrypt.client.standalone_authenticator import \\\n+            unpack_2bytes, unpack_3bytes, pack_2bytes, pack_3bytes\n+        self.assertEqual(unpack_2bytes(\"JZ\"), 19034)\n+        self.assertEqual(unpack_2bytes(chr(0)*2), 0)\n+        self.assertEqual(unpack_2bytes(chr(255)*2), 65535)\n+\n+        self.assertEqual(unpack_3bytes(\"abc\"), 6382179)\n+        self.assertEqual(unpack_3bytes(chr(0)*3), 0)\n+        self.assertEqual(unpack_3bytes(chr(255)*3), 16777215)\n+\n+        self.assertEqual(pack_2bytes(12), chr(0) + chr(12))\n+        self.assertEqual(pack_2bytes(1729), chr(6) + chr(193))\n+\n+        self.assertEqual(pack_3bytes(0), chr(0)*3)\n+        self.assertEqual(pack_3bytes(12345678), chr(0xbc) + \"aN\")\n+\n+    def test_invalid_pack_and_unpack(self):\n+        from letsencrypt.client.standalone_authenticator import \\\n+            unpack_2bytes, unpack_3bytes, pack_2bytes, pack_3bytes\n+        self.assertRaises(AssertionError, pack_2bytes, 65537)\n+        self.assertRaises(AssertionError, pack_3bytes, 500000000)\n+        self.assertRaises(AssertionError, unpack_2bytes, \"foo\")\n+        self.assertRaises(AssertionError, unpack_3bytes, \"food\")\n+\n+\n+class TLSParseClientHelloTest(unittest.TestCase):\n+    # pylint: disable=too-few-public-methods\n+    \"\"\"Test for tls_parse_client_hello() function.\"\"\"\n+    def test_tls_parse_client_hello(self):\n+        from letsencrypt.client.standalone_authenticator import \\\n+            tls_parse_client_hello\n+        client_hello = \"16030100c4010000c003030cfef9971eda442c60cbb6c397\" \\\n+            \"7957a81a8ada317e800b7867a8c61f71c40cab000020c02b\" \\\n+            \"c02fc00ac009c013c014c007c011003300320039002f0035\" \\\n+            \"000a000500040100007700000010000e00000b7777772e65\" \\\n+            \"66662e6f7267ff01000100000a0008000600170018001900\" \\\n+            \"0b00020100002300003374000000100021001f0568322d31\" \\\n+            \"3408737064792f332e3106737064792f3308687474702f31\" \\\n+            \"2e31000500050100000000000d0012001004010501020104\" \\\n+            \"030503020304020202\".decode(\"hex\")\n+        return_value = tls_parse_client_hello(client_hello)\n+        self.assertEqual(return_value, (chr(0xc0) + chr(0x2b), \"www.eff.org\"))\n+        # TODO: The failure cases are extremely numerous and require\n+        #       constructing TLS ClientHello messages that are individually\n+        #       defective or surprising in distinct ways. (Each invalid TLS\n+        #       record is invalid in its own way.)\n+\n+\n+class TLSGenerateServerHelloTest(unittest.TestCase):\n+    # pylint: disable=too-few-public-methods\n+    \"\"\"Tests for tls_generate_server_hello() function.\"\"\"\n+    def test_tls_generate_server_hello(self):\n+        from letsencrypt.client.standalone_authenticator import \\\n+            tls_generate_server_hello\n+        server_hello = tls_generate_server_hello(\"Q!\")\n+        self.assertEqual(server_hello[:11].encode(\"hex\"),\n+                         '160303002a020000260303')\n+        self.assertEqual(server_hello[43:], chr(0) + 'Q!' + chr(0))\n+\n+\n+class TLSGenerateCertMsgTest(unittest.TestCase):\n+    # pylint: disable=too-few-public-methods\n+    \"\"\"Tests for tls_generate_cert_msg() function.\"\"\"\n+    def test_tls_generate_cert_msg(self):\n+        from letsencrypt.client.standalone_authenticator import \\\n+            tls_generate_cert_msg\n+        cert = pkg_resources.resource_string(__name__,\n+                                             'testdata/cert.pem')\n+        cert_msg = tls_generate_cert_msg(cert)\n+        self.assertEqual(cert_msg.encode(\"hex\"),\n+                         \"16030301ec0b0001e80001e50001e2308201de30820188a003\"\n+                         \"02010202020539300d06092a864886f70d01010b0500307731\"\n+                         \"0b30090603550406130255533111300f06035504080c084d69\"\n+                         \"63686967616e3112301006035504070c09416e6e204172626f\"\n+                         \"72312b3029060355040a0c22556e6976657273697479206f66\"\n+                         \"204d6963686967616e20616e64207468652045464631143012\"\n+                         \"06035504030c0b6578616d706c652e636f6d301e170d313431\"\n+                         \"3231313232333434355a170d3134313231383232333434355a\"\n+                         \"3077310b30090603550406130255533111300f06035504080c\"\n+                         \"084d6963686967616e3112301006035504070c09416e6e2041\"\n+                         \"72626f72312b3029060355040a0c22556e6976657273697479\"\n+                         \"206f66204d6963686967616e20616e64207468652045464631\"\n+                         \"14301206035504030c0b6578616d706c652e636f6d305c300d\"\n+                         \"06092a864886f70d0101010500034b003048024100ac7573b4\"\n+                         \"51ed1fddae705243fcdfc75bd02c751b14b875010410e51f03\"\n+                         \"6545dddfa79f34aefdbee90584df471681d9894bce8e6d1cfa\"\n+                         \"9544e8af84744fedc2e50203010001300d06092a864886f70d\"\n+                         \"01010b05000341002db8cf421dc0854a4a59ed92c965bebeb3\"\n+                         \"25ea411f97cc9dd7e4dd7269d748d3e9513ed7828db63874d9\"\n+                         \"ae7a1a8ada02f2404f9fc7ebb13c1af27fa1c36707fa\")\n+\n+\n+class TLSServerHelloDoneTest(unittest.TestCase):\n+    # pylint: disable=too-few-public-methods\n+    \"\"\"Tests for tls_generate_server_hello_done() function.\"\"\"\n+    def test_tls_generate_server_hello_done(self):\n+        from letsencrypt.client.standalone_authenticator import \\\n+            tls_generate_server_hello_done\n+        self.assertEqual(tls_generate_server_hello_done().encode(\"hex\"), \\\n+            \"16030300040e000000\")\n+\n+\n+class ChallPrefTest(unittest.TestCase):\n+    \"\"\"Tests for chall_pref() method.\"\"\"\n+    def setUp(self):\n+        from letsencrypt.client.standalone_authenticator import \\\n+            StandaloneAuthenticator\n+        self.authenticator = StandaloneAuthenticator()\n+\n+    def test_chall_pref(self):\n+        self.assertEqual(self.authenticator.get_chall_pref(\"example.com\"),\n+                         [\"dvsni\"])\n+\n+\n+class SNICallbackTest(unittest.TestCase):\n+    \"\"\"Tests for sni_callback() method.\"\"\"\n+    def setUp(self):\n+        from letsencrypt.client.standalone_authenticator import \\\n+            StandaloneAuthenticator\n+        from letsencrypt.client.challenge_util import dvsni_gen_cert\n+        from letsencrypt.client import le_util\n+        import OpenSSL.crypto\n+        from OpenSSL.crypto import FILETYPE_PEM\n+        self.authenticator = StandaloneAuthenticator()\n+        name, r_b64 = \"example.com\", le_util.jose_b64encode(\"x\" * 32)\n+        test_key = pkg_resources.resource_string(__name__,\n+                                                 'testdata/rsa256_key.pem')\n+        nonce, key = \"abcdef\", le_util.Key(\"foo\", test_key)\n+        self.cert = dvsni_gen_cert(name, r_b64, nonce, key)[0]\n+        private_key = OpenSSL.crypto.load_privatekey(FILETYPE_PEM, key.pem)\n+        self.authenticator.private_key = private_key\n+        self.authenticator.tasks = {\"abcdef.acme.invalid\": self.cert}\n+        self.authenticator.child_pid = 12345\n+\n+    def test_real_servername(self):\n+        import OpenSSL.SSL\n+        connection = mock.MagicMock()\n+        connection.get_servername.return_value = \"abcdef.acme.invalid\"\n+        self.authenticator.sni_callback(connection)\n+        self.assertEqual(connection.set_context.call_count, 1)\n+        called_ctx = connection.set_context.call_args[0][0]\n+        self.assertTrue(isinstance(called_ctx, OpenSSL.SSL.Context))\n+\n+    def test_fake_servername(self):\n+        \"\"\"Test the behavior of the SNI callback when an unexpected SNI\n+        name is received.  (Currently the expected behavior in this case\n+        is to return the \"first\" certificate with which the listener\n+        was configured, although they are stored in an unordered data\n+        structure so this might not be the one that was first in the\n+        challenge list passed to the perform method.  In the future, this\n+        might result in dropping the connection instead.)\"\"\"\n+        import OpenSSL.SSL\n+        connection = mock.MagicMock()\n+        connection.get_servername.return_value = \"example.com\"\n+        self.authenticator.sni_callback(connection)\n+        self.assertEqual(connection.set_context.call_count, 1)\n+        called_ctx = connection.set_context.call_args[0][0]\n+        self.assertTrue(isinstance(called_ctx, OpenSSL.SSL.Context))\n+\n+class ClientSignalHandlerTest(unittest.TestCase):\n+    \"\"\"Tests for client_signal_handler() method.\"\"\"\n+    def setUp(self):\n+        from letsencrypt.client.standalone_authenticator import \\\n+            StandaloneAuthenticator\n+        self.authenticator = StandaloneAuthenticator()\n+        self.authenticator.tasks = {\"foononce.acme.invalid\": \"stuff\"}\n+        self.authenticator.child_pid = 12345\n+\n+    def test_client_signal_handler(self):\n+        import signal\n+        self.assertEqual(self.authenticator.subproc_state, None)\n+        self.authenticator.client_signal_handler(signal.SIGIO, None)\n+        self.assertEqual(self.authenticator.subproc_state, \"ready\")\n+\n+        self.authenticator.client_signal_handler(signal.SIGUSR1, None)\n+        self.assertEqual(self.authenticator.subproc_state, \"inuse\")\n+\n+        self.authenticator.client_signal_handler(signal.SIGUSR2, None)\n+        self.assertEqual(self.authenticator.subproc_state, \"cantbind\")\n+\n+        # Testing the unreached path for a signal other than these\n+        # specified (which can't occur in normal use because this\n+        # function is only set as a signal handler for the above three\n+        # signals).\n+        self.assertRaises(AssertionError,\n+                          self.authenticator.client_signal_handler,\n+                          signal.SIGPIPE, None)\n+\n+\n+class SubprocSignalHandlerTest(unittest.TestCase):\n+    \"\"\"Tests for subproc_signal_handler() method.\"\"\"\n+    def setUp(self):\n+        from letsencrypt.client.standalone_authenticator import \\\n+            StandaloneAuthenticator\n+        self.authenticator = StandaloneAuthenticator()\n+        self.authenticator.tasks = {\"foononce.acme.invalid\": \"stuff\"}\n+        self.authenticator.child_pid = 12345\n+        self.authenticator.parent_pid = 23456\n+\n+    @mock.patch(\"letsencrypt.client.standalone_authenticator.os.kill\")\n+    @mock.patch(\"letsencrypt.client.standalone_authenticator.sys.exit\")\n+    def test_subproc_signal_handler(self, mock_exit, mock_kill):\n+        import signal\n+        self.authenticator.ssl_conn = mock.MagicMock()\n+        self.authenticator.connection = mock.MagicMock()\n+        self.authenticator.sock = mock.MagicMock()\n+        self.authenticator.subproc_signal_handler(signal.SIGINT, None)\n+        self.assertEquals(self.authenticator.ssl_conn.shutdown.call_count, 1)\n+        self.assertEquals(self.authenticator.ssl_conn.close.call_count, 1)\n+        self.assertEquals(self.authenticator.connection.close.call_count, 1)\n+        self.assertEquals(self.authenticator.sock.close.call_count, 1)\n+        mock_kill.assert_called_once_with(self.authenticator.parent_pid,\n+                                          signal.SIGUSR1)\n+        mock_exit.assert_called_once_with(0)\n+\n+    @mock.patch(\"letsencrypt.client.standalone_authenticator.os.kill\")\n+    @mock.patch(\"letsencrypt.client.standalone_authenticator.sys.exit\")\n+    def test_subproc_signal_handler_trouble(self, mock_exit, mock_kill):\n+        \"\"\"Test how the signal handler survives attempting to shut down\n+        a non-existent connection (because none was established or active\n+        at the time the signal handler tried to perform the cleanup).\"\"\"\n+        import signal\n+        self.authenticator.ssl_conn = mock.MagicMock()\n+        self.authenticator.connection = mock.MagicMock()\n+        self.authenticator.sock = mock.MagicMock()\n+        # AttributeError simulates the case where one of these properties\n+        # is None because no connection exists.  We raise it for\n+        # ssl_conn.close() instead of ssl_conn.shutdown() for better code\n+        # coverage.\n+        self.authenticator.ssl_conn.close.side_effect = AttributeError(\"!\")\n+        self.authenticator.connection.close.side_effect = AttributeError(\"!\")\n+        self.authenticator.sock.close.side_effect = AttributeError(\"!\")\n+        self.authenticator.subproc_signal_handler(signal.SIGINT, None)\n+        self.assertEquals(self.authenticator.ssl_conn.shutdown.call_count, 1)\n+        self.assertEquals(self.authenticator.ssl_conn.close.call_count, 1)\n+        self.assertEquals(self.authenticator.connection.close.call_count, 1)\n+        self.assertEquals(self.authenticator.sock.close.call_count, 1)\n+        mock_kill.assert_called_once_with(self.authenticator.parent_pid,\n+                                          signal.SIGUSR1)\n+        mock_exit.assert_called_once_with(0)\n+\n+\n+class PerformTest(unittest.TestCase):\n+    \"\"\"Tests for perform() method.\"\"\"\n+    def setUp(self):\n+        from letsencrypt.client.standalone_authenticator import \\\n+            StandaloneAuthenticator\n+        self.authenticator = StandaloneAuthenticator()\n+\n+    def test_can_perform(self):\n+        \"\"\"What happens if start_listener() returns True.\"\"\"\n+        from letsencrypt.client import le_util\n+        test_key = pkg_resources.resource_string(__name__,\n+                                                 'testdata/rsa256_key.pem')\n+        key = le_util.Key(\"something\", test_key)\n+        chall1 = DvsniChall(\"foo.example.com\", \"whee\", \"foononce\", key)\n+        chall2 = DvsniChall(\"bar.example.com\", \"whee\", \"barnonce\", key)\n+        bad_chall = (\"This\", \"Represents\", \"A Non-DVSNI\", \"Challenge\")\n+        self.authenticator.start_listener = mock.Mock()\n+        self.authenticator.start_listener.return_value = True\n+        result = self.authenticator.perform([chall1, chall2, bad_chall])\n+        self.assertEqual(len(self.authenticator.tasks), 2)\n+        self.assertTrue(\n+            self.authenticator.tasks.has_key(\"foononce.acme.invalid\"))\n+        self.assertTrue(\n+            self.authenticator.tasks.has_key(\"barnonce.acme.invalid\"))\n+        self.assertTrue(isinstance(result, list))\n+        self.assertEqual(len(result), 3)\n+        self.assertTrue(isinstance(result[0], dict))\n+        self.assertTrue(isinstance(result[1], dict))\n+        self.assertFalse(result[2])\n+        self.assertTrue(result[0].has_key(\"s\"))\n+        self.assertTrue(result[1].has_key(\"s\"))\n+        self.authenticator.start_listener.assert_called_once_with(443, key)\n+\n+    def test_cannot_perform(self):\n+        \"\"\"What happens if start_listener() returns False.\"\"\"\n+        from letsencrypt.client import le_util\n+        test_key = pkg_resources.resource_string(__name__,\n+                                                 'testdata/rsa256_key.pem')\n+        key = le_util.Key(\"something\", test_key)\n+        chall1 = DvsniChall(\"foo.example.com\", \"whee\", \"foononce\", key)\n+        chall2 = DvsniChall(\"bar.example.com\", \"whee\", \"barnonce\", key)\n+        bad_chall = (\"This\", \"Represents\", \"A Non-DVSNI\", \"Challenge\")\n+        self.authenticator.start_listener = mock.Mock()\n+        self.authenticator.start_listener.return_value = False\n+        result = self.authenticator.perform([chall1, chall2, bad_chall])\n+        self.assertEqual(len(self.authenticator.tasks), 2)\n+        self.assertTrue(\n+            self.authenticator.tasks.has_key(\"foononce.acme.invalid\"))\n+        self.assertTrue(\n+            self.authenticator.tasks.has_key(\"barnonce.acme.invalid\"))\n+        self.assertTrue(isinstance(result, list))\n+        self.assertEqual(len(result), 3)\n+        self.assertEqual(result, [None, None, False])\n+        self.authenticator.start_listener.assert_called_once_with(443, key)\n+\n+    def test_perform_with_pending_tasks(self):\n+        self.authenticator.tasks = {\"foononce.acme.invalid\": \"cert_data\"}\n+        extra_challenge = DvsniChall(\"a\", \"b\", \"c\", \"d\")\n+        self.assertRaises(Exception, self.authenticator.perform,\n+                          [extra_challenge])\n+\n+    def test_perform_without_challenge_list(self):\n+        extra_challenge = DvsniChall(\"a\", \"b\", \"c\", \"d\")\n+        # This is wrong because a challenge must be specified.\n+        self.assertRaises(Exception, self.authenticator.perform, [])\n+        # This is wrong because it must be a list, not a bare challenge.\n+        self.assertRaises(Exception, self.authenticator.perform,\n+                          extra_challenge)\n+        # This is wrong because the list must contain at least one challenge.\n+        self.assertRaises(Exception, self.authenticator.perform,\n+                          range(20))\n+\n+\n+class StartListenerTest(unittest.TestCase):\n+    \"\"\"Tests for start_listener() method.\"\"\"\n+    def setUp(self):\n+        from letsencrypt.client.standalone_authenticator import \\\n+            StandaloneAuthenticator\n+        self.authenticator = StandaloneAuthenticator()\n+\n+    @mock.patch(\"letsencrypt.client.standalone_authenticator.\"\n+                \"Crypto.Random.atfork\")\n+    @mock.patch(\"letsencrypt.client.standalone_authenticator.os.fork\")\n+    def test_start_listener_fork_parent(self, mock_fork, mock_atfork):\n+        self.authenticator.do_parent_process = mock.Mock()\n+        mock_fork.return_value = 22222\n+        self.authenticator.start_listener(1717, \"key\")\n+        self.assertEqual(self.authenticator.child_pid, 22222)\n+        self.authenticator.do_parent_process.assert_called_once_with(1717)\n+        mock_atfork.assert_called_once_with()\n+\n+    @mock.patch(\"letsencrypt.client.standalone_authenticator.\"\n+                \"Crypto.Random.atfork\")\n+    @mock.patch(\"letsencrypt.client.standalone_authenticator.os.fork\")\n+    def test_start_listener_fork_child(self, mock_fork, mock_atfork):\n+        import os\n+        self.authenticator.do_parent_process = mock.Mock()\n+        self.authenticator.do_child_process = mock.Mock()\n+        mock_fork.return_value = 0\n+        self.authenticator.start_listener(1717, \"key\")\n+        self.assertEqual(self.authenticator.child_pid, os.getpid())\n+        self.authenticator.do_child_process.assert_called_once_with(1717,\n+                                                                    \"key\")\n+        mock_atfork.assert_called_once_with()\n+\n+class DoParentProcessTest(unittest.TestCase):\n+    \"\"\"Tests for do_parent_process() method.\"\"\"\n+    def setUp(self):\n+        from letsencrypt.client.standalone_authenticator import \\\n+            StandaloneAuthenticator\n+        self.authenticator = StandaloneAuthenticator()\n+\n+    @mock.patch(\"letsencrypt.client.standalone_authenticator.signal.signal\")\n+    @mock.patch(\"letsencrypt.client.standalone_authenticator.\"\n+                \"zope.component.getUtility\")\n+    def test_do_parent_process_ok(self, mock_get_utility, mock_signal):\n+        self.authenticator.subproc_state = \"ready\"\n+        result = self.authenticator.do_parent_process(1717)\n+        self.assertTrue(result)\n+        self.assertEqual(mock_get_utility.call_count, 1)\n+        self.assertEqual(mock_signal.call_count, 3)\n+\n+    @mock.patch(\"letsencrypt.client.standalone_authenticator.signal.signal\")\n+    @mock.patch(\"letsencrypt.client.standalone_authenticator.\"\n+                \"zope.component.getUtility\")\n+    def test_do_parent_process_inuse(self, mock_get_utility, mock_signal):\n+        self.authenticator.subproc_state = \"inuse\"\n+        result = self.authenticator.do_parent_process(1717)\n+        self.assertFalse(result)\n+        self.assertEqual(mock_get_utility.call_count, 1)\n+        self.assertEqual(mock_signal.call_count, 3)\n+\n+    @mock.patch(\"letsencrypt.client.standalone_authenticator.signal.signal\")\n+    @mock.patch(\"letsencrypt.client.standalone_authenticator.\"\n+                \"zope.component.getUtility\")\n+    def test_do_parent_process_cantbind(self, mock_get_utility, mock_signal):\n+        self.authenticator.subproc_state = \"cantbind\"\n+        result = self.authenticator.do_parent_process(1717)\n+        self.assertFalse(result)\n+        self.assertEqual(mock_get_utility.call_count, 1)\n+        self.assertEqual(mock_signal.call_count, 3)\n+\n+    @mock.patch(\"letsencrypt.client.standalone_authenticator.signal.signal\")\n+    @mock.patch(\"letsencrypt.client.standalone_authenticator.\"\n+                \"zope.component.getUtility\")\n+    def test_do_parent_process_timeout(self, mock_get_utility, mock_signal):\n+        # Normally times out in 5 seconds and returns False.  We can\n+        # now set delay_amount to a lower value so that it times out\n+        # faster than it would under normal use.\n+        result = self.authenticator.do_parent_process(1717, delay_amount=1)\n+        self.assertFalse(result)\n+        self.assertEqual(mock_get_utility.call_count, 1)\n+        self.assertEqual(mock_signal.call_count, 3)\n+\n+\n+class DoChildProcessTest(unittest.TestCase):\n+    \"\"\"Tests for do_child_process() method.\"\"\"\n+    def setUp(self):\n+        from letsencrypt.client.standalone_authenticator import \\\n+            StandaloneAuthenticator\n+        from letsencrypt.client.challenge_util import dvsni_gen_cert\n+        from letsencrypt.client import le_util\n+        import OpenSSL.crypto\n+        from OpenSSL.crypto import FILETYPE_PEM\n+        self.authenticator = StandaloneAuthenticator()\n+        name, r_b64 = \"example.com\", le_util.jose_b64encode(\"x\" * 32)\n+        test_key = pkg_resources.resource_string(__name__,\n+                                                 'testdata/rsa256_key.pem')\n+        nonce, key = \"abcdef\", le_util.Key(\"foo\", test_key)\n+        self.key = key\n+        self.cert = dvsni_gen_cert(name, r_b64, nonce, key)[0]\n+        private_key = OpenSSL.crypto.load_privatekey(FILETYPE_PEM, key.pem)\n+        self.authenticator.private_key = private_key\n+        self.authenticator.tasks = {\"abcdef.acme.invalid\": self.cert}\n+        self.authenticator.parent_pid = 12345\n+\n+    @mock.patch(\"letsencrypt.client.standalone_authenticator.socket.socket\")\n+    @mock.patch(\"letsencrypt.client.standalone_authenticator.os.kill\")\n+    @mock.patch(\"letsencrypt.client.standalone_authenticator.sys.exit\")\n+    def test_do_child_process_cantbind1(self, mock_exit, mock_kill,\n+                                        mock_socket):\n+        import socket, signal\n+        mock_exit.side_effect = IndentationError(\"subprocess would exit here\")\n+        eaccess = socket.error(socket.errno.EACCES, \"Permission denied\")\n+        sample_socket = mock.MagicMock()\n+        sample_socket.bind.side_effect = eaccess\n+        mock_socket.return_value = sample_socket\n+        # Using the IndentationError as an error that cannot easily be\n+        # generated at runtime, to indicate the behavior of sys.exit has\n+        # taken effect without actually causing the test process to exit.\n+        # (Just replacing it with a no-op causes logic errors because the\n+        # do_child_process code assumes that calling sys.exit() will\n+        # cause subsequent code not to be executed.)\n+        self.assertRaises(IndentationError,\n+                          self.authenticator.do_child_process, 1717, self.key)\n+        mock_exit.assert_called_once_with(1)\n+        mock_kill.assert_called_once_with(12345, signal.SIGUSR2)\n+\n+    @mock.patch(\"letsencrypt.client.standalone_authenticator.socket.socket\")\n+    @mock.patch(\"letsencrypt.client.standalone_authenticator.os.kill\")\n+    @mock.patch(\"letsencrypt.client.standalone_authenticator.sys.exit\")\n+    def test_do_child_process_cantbind2(self, mock_exit, mock_kill,\n+                                        mock_socket):\n+        import socket, signal\n+        mock_exit.side_effect = IndentationError(\"subprocess would exit here\")\n+        eaccess = socket.error(socket.errno.EADDRINUSE, \"Port already in use\")\n+        sample_socket = mock.MagicMock()\n+        sample_socket.bind.side_effect = eaccess\n+        mock_socket.return_value = sample_socket\n+        self.assertRaises(IndentationError,\n+                          self.authenticator.do_child_process, 1717, self.key)\n+        mock_exit.assert_called_once_with(1)\n+        mock_kill.assert_called_once_with(12345, signal.SIGUSR1)\n+\n+    @mock.patch(\"letsencrypt.client.standalone_authenticator.socket.socket\")\n+    def test_do_child_process_cantbind3(self, mock_socket):\n+        \"\"\"Test case where attempt to bind socket results in an unhandled\n+        socket error.  (The expected behavior is arguably wrong because it\n+        will crash the program; the reason for the expected behavior is\n+        that we don't have a way to report arbitrary socket errors.)\"\"\"\n+        import socket\n+        eio = socket.error(socket.errno.EIO, \"Imaginary unhandled error\")\n+        sample_socket = mock.MagicMock()\n+        sample_socket.bind.side_effect = eio\n+        mock_socket.return_value = sample_socket\n+        self.assertRaises(socket.error,\n+                          self.authenticator.do_child_process, 1717, self.key)\n+\n+    @mock.patch(\"letsencrypt.client.standalone_authenticator.\"\n+                \"OpenSSL.SSL.Connection\")\n+    @mock.patch(\"letsencrypt.client.standalone_authenticator.socket.socket\")\n+    @mock.patch(\"letsencrypt.client.standalone_authenticator.os.kill\")\n+    def test_do_child_process_success(self, mock_kill, mock_socket,\n+                                      mock_connection):\n+        import signal","path":"letsencrypt/client/tests/standalone_authenticator_test.py","position":521,"original_position":521,"commit_id":"b324f9d9125cc1b8c164110ba671abcb47b86d1e","original_commit_id":"b324f9d9125cc1b8c164110ba671abcb47b86d1e","user":{"login":"kuba","id":106176,"avatar_url":"https://avatars.githubusercontent.com/u/106176?v=3","gravatar_id":"","url":"https://api.github.com/users/kuba","html_url":"https://github.com/kuba","followers_url":"https://api.github.com/users/kuba/followers","following_url":"https://api.github.com/users/kuba/following{/other_user}","gists_url":"https://api.github.com/users/kuba/gists{/gist_id}","starred_url":"https://api.github.com/users/kuba/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/kuba/subscriptions","organizations_url":"https://api.github.com/users/kuba/orgs","repos_url":"https://api.github.com/users/kuba/repos","events_url":"https://api.github.com/users/kuba/events{/privacy}","received_events_url":"https://api.github.com/users/kuba/received_events","type":"User","site_admin":false},"body":"import at the top","created_at":"2015-02-09T17:38:25Z","updated_at":"2015-02-09T17:38:25Z","html_url":"https://github.com/letsencrypt/lets-encrypt-preview/pull/232#discussion_r24347514","pull_request_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/pulls/232","_links":{"self":{"href":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/pulls/comments/24347514"},"html":{"href":"https://github.com/letsencrypt/lets-encrypt-preview/pull/232#discussion_r24347514"},"pull_request":{"href":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/pulls/232"}}},"pull_request":{"url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/pulls/232","id":28778019,"html_url":"https://github.com/letsencrypt/lets-encrypt-preview/pull/232","diff_url":"https://github.com/letsencrypt/lets-encrypt-preview/pull/232.diff","patch_url":"https://github.com/letsencrypt/lets-encrypt-preview/pull/232.patch","issue_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/issues/232","number":232,"state":"open","locked":false,"title":"Standalone authenticator","user":{"login":"schoen","id":1750103,"avatar_url":"https://avatars.githubusercontent.com/u/1750103?v=3","gravatar_id":"","url":"https://api.github.com/users/schoen","html_url":"https://github.com/schoen","followers_url":"https://api.github.com/users/schoen/followers","following_url":"https://api.github.com/users/schoen/following{/other_user}","gists_url":"https://api.github.com/users/schoen/gists{/gist_id}","starred_url":"https://api.github.com/users/schoen/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/schoen/subscriptions","organizations_url":"https://api.github.com/users/schoen/orgs","repos_url":"https://api.github.com/users/schoen/repos","events_url":"https://api.github.com/users/schoen/events{/privacy}","received_events_url":"https://api.github.com/users/schoen/received_events","type":"User","site_admin":false},"body":"This branch implements a standalone authenticator, part of the so-called standalone configurator or null configurator functionality.  The authenticator uses the plugin API and can start a TCP listener to complete DVSNI challenges without the use of a web server.\r\n\r\nThere are unit tests for all functions, including some functions that are not currently called, with pretty complete code coverage (a few failure cases are not yet tested).  We also found that this can be substituted in place of the Apache configurator's authenticator, and we do get all the way to certificate issuance and deployment.\r\n\r\nThere is currently no way to choose to use the standalone authenticator from inside of the user interface (or at all).  James and I have been talking about this; there will need to be UI changes in the main LE client in order to be able to choose this instead of the Apache configurator and perhaps ultimately to detect automatically which configurator ought to be used.\r\n\r\nTwo things for discussion, which don't necessarily need to be resolved prior to merging:\r\n\r\n* The strategy of spawning a subprocess for the listener and doing minimal IPC with Unix signals.  This doesn't give particularly rich error reporting if something goes wrong with the listener process.\r\n* The \"abbreviated DVSNI\" functionality (implementing a small subset of TLS messages manually in order to complete only the part of the TLS handshake that is used by DVSNI, and not expecting or requiring to complete the handshake all the way to Finished, instead disconnecting once the DVSNI certificate has been offered and received).  Right now there is a complete and working abbreviated abbreviated DVSNI implementation but it doesn't interoperate with the Node server, which expects to complete the TLS handshake when verifying a proof.  So the abbreviate DVSNI logic is commented out and the listener instead currently uses the PyOpenSSL binding to perform a TLS handshake.\r\n\r\nChoosing to use abbreviated DVSNI would require making changes on the verifier side too (for interoperability), as well as a judgment that this is indeed safer than using bindings for full-fledged TLS libraries on both sides.","created_at":"2015-02-06T01:26:04Z","updated_at":"2015-02-09T17:38:25Z","closed_at":null,"merged_at":null,"merge_commit_sha":"4d2c33727bd7bda56b3b17a46bdab0a25f06df2e","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/pulls/232/commits","review_comments_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/pulls/232/comments","review_comment_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/pulls/comments/{number}","comments_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/issues/232/comments","statuses_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/statuses/b324f9d9125cc1b8c164110ba671abcb47b86d1e","head":{"label":"letsencrypt:standalone_authenticator","ref":"standalone_authenticator","sha":"b324f9d9125cc1b8c164110ba671abcb47b86d1e","user":{"login":"letsencrypt","id":9289019,"avatar_url":"https://avatars.githubusercontent.com/u/9289019?v=3","gravatar_id":"","url":"https://api.github.com/users/letsencrypt","html_url":"https://github.com/letsencrypt","followers_url":"https://api.github.com/users/letsencrypt/followers","following_url":"https://api.github.com/users/letsencrypt/following{/other_user}","gists_url":"https://api.github.com/users/letsencrypt/gists{/gist_id}","starred_url":"https://api.github.com/users/letsencrypt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/letsencrypt/subscriptions","organizations_url":"https://api.github.com/users/letsencrypt/orgs","repos_url":"https://api.github.com/users/letsencrypt/repos","events_url":"https://api.github.com/users/letsencrypt/events{/privacy}","received_events_url":"https://api.github.com/users/letsencrypt/received_events","type":"Organization","site_admin":false},"repo":{"id":26516210,"name":"lets-encrypt-preview","full_name":"letsencrypt/lets-encrypt-preview","owner":{"login":"letsencrypt","id":9289019,"avatar_url":"https://avatars.githubusercontent.com/u/9289019?v=3","gravatar_id":"","url":"https://api.github.com/users/letsencrypt","html_url":"https://github.com/letsencrypt","followers_url":"https://api.github.com/users/letsencrypt/followers","following_url":"https://api.github.com/users/letsencrypt/following{/other_user}","gists_url":"https://api.github.com/users/letsencrypt/gists{/gist_id}","starred_url":"https://api.github.com/users/letsencrypt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/letsencrypt/subscriptions","organizations_url":"https://api.github.com/users/letsencrypt/orgs","repos_url":"https://api.github.com/users/letsencrypt/repos","events_url":"https://api.github.com/users/letsencrypt/events{/privacy}","received_events_url":"https://api.github.com/users/letsencrypt/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/letsencrypt/lets-encrypt-preview","description":"An ACME client that can update Apache configurations (developer preview) ","fork":false,"url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview","forks_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/forks","keys_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/keys{/key_id}","collaborators_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/teams","hooks_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/hooks","issue_events_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/issues/events{/number}","events_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/events","assignees_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/assignees{/user}","branches_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/branches{/branch}","tags_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/tags","blobs_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/git/refs{/sha}","trees_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/git/trees{/sha}","statuses_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/statuses/{sha}","languages_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/languages","stargazers_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/stargazers","contributors_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/contributors","subscribers_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/subscribers","subscription_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/subscription","commits_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/commits{/sha}","git_commits_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/git/commits{/sha}","comments_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/comments{/number}","issue_comment_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/issues/comments/{number}","contents_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/contents/{+path}","compare_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/compare/{base}...{head}","merges_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/merges","archive_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/downloads","issues_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/issues{/number}","pulls_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/pulls{/number}","milestones_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/milestones{/number}","notifications_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/labels{/name}","releases_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/releases{/id}","created_at":"2014-11-12T02:52:20Z","updated_at":"2015-02-09T16:30:42Z","pushed_at":"2015-02-09T17:27:06Z","git_url":"git://github.com/letsencrypt/lets-encrypt-preview.git","ssh_url":"git@github.com:letsencrypt/lets-encrypt-preview.git","clone_url":"https://github.com/letsencrypt/lets-encrypt-preview.git","svn_url":"https://github.com/letsencrypt/lets-encrypt-preview","homepage":null,"size":4464,"stargazers_count":1405,"watchers_count":1405,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":91,"mirror_url":null,"open_issues_count":47,"forks":91,"open_issues":47,"watchers":1405,"default_branch":"master"}},"base":{"label":"letsencrypt:master","ref":"master","sha":"bbd5c8ef2aa28e61cc8fae6e63f1bc7c5df4c033","user":{"login":"letsencrypt","id":9289019,"avatar_url":"https://avatars.githubusercontent.com/u/9289019?v=3","gravatar_id":"","url":"https://api.github.com/users/letsencrypt","html_url":"https://github.com/letsencrypt","followers_url":"https://api.github.com/users/letsencrypt/followers","following_url":"https://api.github.com/users/letsencrypt/following{/other_user}","gists_url":"https://api.github.com/users/letsencrypt/gists{/gist_id}","starred_url":"https://api.github.com/users/letsencrypt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/letsencrypt/subscriptions","organizations_url":"https://api.github.com/users/letsencrypt/orgs","repos_url":"https://api.github.com/users/letsencrypt/repos","events_url":"https://api.github.com/users/letsencrypt/events{/privacy}","received_events_url":"https://api.github.com/users/letsencrypt/received_events","type":"Organization","site_admin":false},"repo":{"id":26516210,"name":"lets-encrypt-preview","full_name":"letsencrypt/lets-encrypt-preview","owner":{"login":"letsencrypt","id":9289019,"avatar_url":"https://avatars.githubusercontent.com/u/9289019?v=3","gravatar_id":"","url":"https://api.github.com/users/letsencrypt","html_url":"https://github.com/letsencrypt","followers_url":"https://api.github.com/users/letsencrypt/followers","following_url":"https://api.github.com/users/letsencrypt/following{/other_user}","gists_url":"https://api.github.com/users/letsencrypt/gists{/gist_id}","starred_url":"https://api.github.com/users/letsencrypt/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/letsencrypt/subscriptions","organizations_url":"https://api.github.com/users/letsencrypt/orgs","repos_url":"https://api.github.com/users/letsencrypt/repos","events_url":"https://api.github.com/users/letsencrypt/events{/privacy}","received_events_url":"https://api.github.com/users/letsencrypt/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/letsencrypt/lets-encrypt-preview","description":"An ACME client that can update Apache configurations (developer preview) ","fork":false,"url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview","forks_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/forks","keys_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/keys{/key_id}","collaborators_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/teams","hooks_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/hooks","issue_events_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/issues/events{/number}","events_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/events","assignees_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/assignees{/user}","branches_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/branches{/branch}","tags_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/tags","blobs_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/git/refs{/sha}","trees_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/git/trees{/sha}","statuses_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/statuses/{sha}","languages_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/languages","stargazers_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/stargazers","contributors_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/contributors","subscribers_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/subscribers","subscription_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/subscription","commits_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/commits{/sha}","git_commits_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/git/commits{/sha}","comments_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/comments{/number}","issue_comment_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/issues/comments/{number}","contents_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/contents/{+path}","compare_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/compare/{base}...{head}","merges_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/merges","archive_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/downloads","issues_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/issues{/number}","pulls_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/pulls{/number}","milestones_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/milestones{/number}","notifications_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/labels{/name}","releases_url":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/releases{/id}","created_at":"2014-11-12T02:52:20Z","updated_at":"2015-02-09T16:30:42Z","pushed_at":"2015-02-09T17:27:06Z","git_url":"git://github.com/letsencrypt/lets-encrypt-preview.git","ssh_url":"git@github.com:letsencrypt/lets-encrypt-preview.git","clone_url":"https://github.com/letsencrypt/lets-encrypt-preview.git","svn_url":"https://github.com/letsencrypt/lets-encrypt-preview","homepage":null,"size":4464,"stargazers_count":1405,"watchers_count":1405,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":91,"mirror_url":null,"open_issues_count":47,"forks":91,"open_issues":47,"watchers":1405,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/pulls/232"},"html":{"href":"https://github.com/letsencrypt/lets-encrypt-preview/pull/232"},"issue":{"href":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/issues/232"},"comments":{"href":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/issues/232/comments"},"review_comments":{"href":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/pulls/232/comments"},"review_comment":{"href":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/pulls/comments/{number}"},"commits":{"href":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/pulls/232/commits"},"statuses":{"href":"https://api.github.com/repos/letsencrypt/lets-encrypt-preview/statuses/b324f9d9125cc1b8c164110ba671abcb47b86d1e"}}}}