title: how to setup a aiohttp https server and client?
body: <p>I'm trying to learn how I might secure data from being altered after being passed over an open network between a server and a worker</p>
  
  <p>in my head I was thinking that it should follow something like:</p>
  
  <pre><code>|server|---send_job-----&gt;|worker|
  |      |&lt;--send_results--|      |
  |      |                 |      |
  |      |-send_kill_req--&gt;|      |
  </code></pre>
  
  <p>obviously I don't want someone altering my <code>send_job</code> to do something nefarious, and I don't want someone to be peeking at my results.</p>
  
  <p>so I have a super simple <code>aiohttp</code> client/server setup that I'm trying to implement <code>ssl</code> into but I'm completely lost. </p>
  
  <p>below is the most basic stuff I've tried, but I've also tried implementing my own ssl certificates via:</p>
  
  <pre><code>openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -keyout domain_srv.key -out domain_srv.crt
  </code></pre>
  
  <p>along with following the <a href="http://docs.aiohttp.org/en/stable/client_advanced.html?highlight=ssl#ssl-control-for-tcp-sockets" rel="noreferrer">documentation</a> but I'm still never able to <code>get</code> any response at all.</p>
  
  <p>How would I properly implement the <code>ssl_context</code> to get this to work?!</p>
  
  <p><code>server.py</code></p>
  
  <pre><code>from aiohttp import web
  import msgpack
  import ssl
  
  async def handle(request):
      name = request.match_info.get('name', "Anonymous")
      text = "Hello, " + name
      return web.Response(text=text)
  
  app = web.Application()
  app.add_routes([web.get('/', handle),
                  web.get('/{name}', handle)])
  
  ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
  web.run_app(app, ssl_context=ssl_context)
  </code></pre>
  
  <p><code>client.py</code>
      import aiohttp
      import asyncio
      import ssl</p>
  
  <pre><code>async def main():
      sslcontext = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH)
      async with aiohttp.ClientSession() as session:
          async with session.get("https://0.0.0.0:8443", ssl=sslcontext) as response:
              html = await response.read()
              print(html)
  
  loop = asyncio.get_event_loop()
  loop.run_until_complete(main())
  </code></pre>
  
  <ol>
  <li>run <code>python3 server.py</code> in one window</li>
  <li>run <code>python3 client.py</code> in another window</li>
  </ol>
  
  <p>I then end up usually with something like:</p>
  
  <pre><code>Traceback (most recent call last):
    File "/home/mEE/miniconda3/envs/py3/lib/python3.6/site-packages/aiohttp/connector.py", line 822, in _wrap_create_connection
      return await self._loop.create_connection(*args, **kwargs)
    File "/home/mEE/miniconda3/envs/py3/lib/python3.6/asyncio/base_events.py", line 804, in create_connection
      sock, protocol_factory, ssl, server_hostname)
    File "/home/mEE/miniconda3/envs/py3/lib/python3.6/asyncio/base_events.py", line 830, in _create_connection_transport
      yield from waiter
  ConnectionResetError
  
  The above exception was the direct cause of the following exception:
  
  Traceback (most recent call last):
    File "client.py", line 14, in &lt;module&gt;
      loop.run_until_complete(main())
    File "/home/mEE/miniconda3/envs/py3/lib/python3.6/asyncio/base_events.py", line 468, in run_until_complete
      return future.result()
    File "client.py", line 9, in main
      async with session.get("https://0.0.0.0:8443", ssl=sslcontext) as response:
    File "/home/mEE/miniconda3/envs/py3/lib/python3.6/site-packages/aiohttp/client.py", line 843, in __aenter__
      self._resp = await self._coro
    File "/home/mEE/miniconda3/envs/py3/lib/python3.6/site-packages/aiohttp/client.py", line 366, in _request
      timeout=timeout
    File "/home/mEE/miniconda3/envs/py3/lib/python3.6/site-packages/aiohttp/connector.py", line 445, in connect
      proto = await self._create_connection(req, traces, timeout)
    File "/home/mEE/miniconda3/envs/py3/lib/python3.6/site-packages/aiohttp/connector.py", line 757, in _create_connection
      req, traces, timeout)
    File "/home/mEE/miniconda3/envs/py3/lib/python3.6/site-packages/aiohttp/connector.py", line 879, in _create_direct_connection
      raise last_exc
    File "/home/mEE/miniconda3/envs/py3/lib/python3.6/site-packages/aiohttp/connector.py", line 862, in _create_direct_connection
      req=req, client_error=client_error)
    File "/home/mEE/miniconda3/envs/py3/lib/python3.6/site-packages/aiohttp/connector.py", line 829, in _wrap_create_connection
      raise client_error(req.connection_key, exc) from exc
  aiohttp.client_exceptions.ClientConnectorError: Cannot connect to host 0.0.0.0:8443 ssl:&lt;ssl.SSLContext object at 0x7fe4800d2278&gt; [None]
  </code></pre>
  
  <hr>
  
  <h2>Solution:</h2>
  
  <p>This was a two part problem,</p>
  
  <ol>
  <li><p>I had no idea what I was doing with openssl, the requests library helped me figure this out!</p>
  
  <pre><code>import requests
  requests.get("https://0.0.0.0:8443", verify="domain_srv.crt")
  
  SSLError: HTTPSConnectionPool(host='0.0.0.0', port=8443): Max retries exceeded with url: / (Caused by SSLError(CertificateError("hostname '0.0.0.0' doesn't match None",),))
  </code></pre>
  
  <p>As it turns out, those lines I just made default when making my openssl certificates actually mattered. A slightly more correct (but probably still wrong) config similar to</p>
  
  <pre><code>Country Name (2 letter code) [AU]:US
  State or Province Name (full name) [Some-State]:.
  Locality Name (eg, city) []:.
  Organization Name (eg, company) [Internet Widgits Pty Ltd]:.
  Organizational Unit Name (eg, section) []:.
  Common Name (e.g. server FQDN or YOUR name) []:0.0.0.0
  Email Address []:.
  </code></pre>
  
  <p>led me to the result:</p>
  
  <pre><code>import requests
  requests.get("https://0.0.0.0:8443", verify="domain_srv.crt")
  
  SubjectAltNameWarning: Certificate for 0.0.0.0 has no `subjectAltName`, falling back to check for a `commonName` for now. This feature is being removed by major browsers and deprecated by RFC 2818. (See https://github.com/shazow/urllib3/issues/497 for details.)
  </code></pre>
  
  <p>It would appear that 'subjectAltName' is something a little more difficult to add, requiring a lot more work than a simple command, you'll want to follow a guide like <a href="https://gist.github.com/jchandra74/36d5f8d0e11960dd8f80260801109ab0" rel="noreferrer">this</a>, I will try it and see if that error goes away.</p></li>
  <li><p>I think I was using <code>ssl.Purpose.CLIENT/SERVER_AUTH</code> wrongly, as @Andrej mentioned, I switched that around (as I will show below) and made a few other changes and now I am getting the correct responses.  I'll just say that, I definitely still don't understand <code>ssl.Purpose</code> but at least I have something that I can work with for now, and hopefully I'll figure out the rest in time.<br>
  <code>client.py</code></p>
  
  <pre><code>import aiohttp
  import asyncio
  import ssl
  
  
  async def main():
      sslcontext = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile='domain_srv.crt')
      async with aiohttp.ClientSession() as session:
          async with session.get("https://0.0.0.0:8443/JOHNYY", ssl=sslcontext) as response:
              html = await response.read()
              print(html)
  
  loop = asyncio.get_event_loop()
  loop.run_until_complete(main())
  </code></pre>
  
  <p><code>server.py</code></p>
  
  <pre><code>from aiohttp import web
  import ssl
  
  async def handle(request):
      name = request.match_info.get('name', "Anonymous")
      text = "Hello, " + name
      return web.Response(text=text)
  
  app = web.Application()
  app.add_routes([web.get('/', handle),
                  web.get('/{name}', handle)])
  
  ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
  ssl_context.load_cert_chain('domain_srv.crt', 'domain_srv.key')
  web.run_app(app, ssl_context=ssl_context)
  </code></pre>
  
  <p><code>commandline</code></p>
  
  <pre><code>&gt;python3 server.py
  # Switch to a new window/pane
  &gt;python3 client.py
  b'Hello, JOHNYY'
  </code></pre></li>
  </ol>
  
  <p><strong>EDIT</strong>: 
  I just wanted to post an update for anyone that's working on this type of problem.  I think that using the python cryptography library is a nicer way of generating the crt/key files so if you're interested feel free to use/modify this template (I make no promise that these are best practices):</p>
  
  <pre><code>#!/usr/bin/env python
  """
  stuff for network security
  """
  
  import socket
  import datetime
  
  from cryptography.hazmat.backends import default_backend
  from cryptography.hazmat.primitives import serialization
  from cryptography.hazmat.primitives.asymmetric import rsa
  from cryptography import x509
  from cryptography.x509.oid import NameOID
  from cryptography.hazmat.primitives import hashes
  
  import attr
  
  
  @attr.s(auto_attribs=True)
  class Netsec:
      hostname: str = attr.Factory(socket.gethostname)
      out_file_name: str = "domain_srv"
  
      def generate_netsec(self):
      # GENERATE KEY
          key = rsa.generate_private_key(
              public_exponent=65537,
              key_size=2048,
              backend=default_backend(),
          )
  
          with open(f"{self.out_file_name}.key", "wb") as f:
              f.write(key.private_bytes(
                  encoding=serialization.Encoding.PEM,
                  format=serialization.PrivateFormat.TraditionalOpenSSL,
                  encryption_algorithm=serialization.NoEncryption(),
              ))
  
          subject = issuer = x509.Name([
              x509.NameAttribute(NameOID.COUNTRY_NAME, u"US"),
              x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, u"CA"),
              x509.NameAttribute(NameOID.LOCALITY_NAME, u"Wala Wala"),
              x509.NameAttribute(NameOID.ORGANIZATION_NAME, u"A place"),
              x509.NameAttribute(NameOID.COMMON_NAME, self.hostname),
          ])
  
          cert = x509.CertificateBuilder().subject_name(
              subject
          ).issuer_name(
              issuer
          ).public_key(
              key.public_key()
          ).serial_number(
              x509.random_serial_number()
          ).not_valid_before(
              datetime.datetime.utcnow()
          ).not_valid_after(
              # Our certificate will be valid for 5 years
              datetime.datetime.utcnow() + datetime.timedelta(days=365*5)
          ).add_extension(
              x509.SubjectAlternativeName([
                  x509.DNSName(u"localhost"),
                  x509.DNSName(self.hostname),
                  x509.DNSName(u"127.0.0.1")]),
              critical=False,
          # Sign our certificate with our private key
          ).sign(key, hashes.SHA256(), default_backend())
  
          with open(f"{self.out_file_name}.crt", "wb") as f:
              f.write(cert.public_bytes(serialization.Encoding.PEM))
  </code></pre>