Tuesday, December 6, 2011

SSL encryption in python bottle

Bottle is a really powerful tool to develop light weight applications quickly.  Recently I was programming little web service with bottle. Something easy. The point is that my service required SSL encryption to have a little more security and, after some googling, I couldn't find any quick&easy howto. However I looked into the bottle code an realised that there would be a good way of doing such thing... and here it is.

Well, the first thing is to know that bottle can use several web servers as backends. So the way of bottle supporting SSL is that one of those servers support it. By default, it uses the wsgiref server that does not support SSL.
On the other hand, one of the biggest benefits of bottle is that is very lightweight: the python-bottle debian package uses only 176KB so, if I have to use other web server than default as backend, I don't want to be a heavy one. Finally I decided to use cherrypy, as it seemed to be supported... but not the way I thought.

There are two debian packages that provide cherrypy: python-cherrypy and python-cherrypy3.  The first one uses 920KB of disk and the second one 15MB. Only the second is supported by bottle by default. Wow!  I have an application that uses about 200KB of disk (bottle included) and should I use 15MB of disk to give it SSL support?  Of course not!

Lets see some code. This is a sample of my application.


#!/usr/bin/env python
#-*- coding:utf-8 -*-


from bottle import Bottle, run, request

app = Bottle()

@app.post('/login')
def login():
    name = request.forms.get('name')
    password = request.forms.get('password')
    if (name, password) == ('root','hackme'):
        return "<p>Your login was correct</p>"
    else:
        return "<p>Login failed</p>"


run(app, host='localhost', port='8080')


Yes, I'm kidding, it's not a sample of my application but a copy&paste of bottle tutorial... O:).

Well, lets exec this server and test it:
  1. Terminal 1: Server running
  2. Terminal 2: Testing server with curl
  3. Terminal 3: Tcpdump watching
Terminal 1:

 $ ./app1.py
Bottle server starting up (using WSGIRefServer())...
Listening on http://localhost:8080/
Use Ctrl-C to quit.

localhost - - [06/Dec/2011 22:44:12] "POST /login HTTP/1.1" 200 19

Terminal 2:


$ curl -d 'name=root&password=hackme' http://localhost:8080/login
<p>Your login was correct</p>



Terminal 3:

# tcpdump -i lo dst port 8080 -XXX
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on lo, link-type EN10MB (Ethernet), capture size 65535 bytes
22:44:12.469205 IP localhost.38409 > localhost.http-alt: Flags [S], seq 887660788, win 32792, options [mss 16396,sackOK,TS val 735295 ecr 0,nop,wscale 4], length 0
    0x0000:  0000 0000 0000 0000 0000 0000 0800 4500  ..............E.
    0x0010:  003c 98cb 4000 4006 a3ee 7f00 0001 7f00  .<..@.@.........
    0x0020:  0001 9609 1f90 34e8 a0f4 0000 0000 a002  ......4.........
    0x0030:  8018 fe30 0000 0204 400c 0402 080a 000b  ...0....@.......
    0x0040:  383f 0000 0000 0103 0304                 8?........
22:44:12.469246 IP localhost.38409 > localhost.http-alt: Flags [.], ack 1094336116, win 2050, options [nop,nop,TS val 735295 ecr 735295], length 0
    0x0000:  0000 0000 0000 0000 0000 0000 0800 4500  ..............E.
    0x0010:  0034 98cc 4000 4006 a3f5 7f00 0001 7f00  .4..@.@.........
    0x0020:  0001 9609 1f90 34e8 a0f5 413a 3e74 8010  ......4...A:>t..
    0x0030:  0802 fe28 0000 0101 080a 000b 383f 000b  ...(........8?..
    0x0040:  383f                                     8?
22:44:12.469377 IP localhost.38409 > localhost.http-alt: Flags [P.], seq 0:265, ack 1, win 2050, options [nop,nop,TS val 735295 ecr 735295], length 265
    0x0000:  0000 0000 0000 0000 0000 0000 0800 4500  ..............E.
    0x0010:  013d 98cd 4000 4006 a2eb 7f00 0001 7f00  .=..@.@.........
    0x0020:  0001 9609 1f90 34e8 a0f5 413a 3e74 8018  ......4...A:>t..
    0x0030:  0802 ff31 0000 0101 080a 000b 383f 000b  ...1........8?..
    0x0040:  383f 504f 5354 202f 6c6f 6769 6e20 4854  8?POST./login.HT
    0x0050:  5450 2f31 2e31 0d0a 5573 6572 2d41 6765  TP/1.1..User-Age
    0x0060:  6e74 3a20 6375 726c 2f37 2e32 312e 3620  nt:.curl/7.21.6.
    0x0070:  2869 3638 362d 7063 2d6c 696e 7578 2d67  (i686-pc-linux-g
    0x0080:  6e75 2920 6c69 6263 7572 6c2f 372e 3231  nu).libcurl/7.21
    0x0090:  2e36 204f 7065 6e53 534c 2f31 2e30 2e30  .6.OpenSSL/1.0.0
    0x00a0:  6520 7a6c 6962 2f31 2e32 2e33 2e34 206c  e.zlib/1.2.3.4.l
    0x00b0:  6962 6964 6e2f 312e 3232 206c 6962 7274  ibidn/1.22.librt
    0x00c0:  6d70 2f32 2e33 0d0a 486f 7374 3a20 6c6f  mp/2.3..Host:.lo
    0x00d0:  6361 6c68 6f73 743a 3830 3830 0d0a 4163  calhost:8080..Ac
    0x00e0:  6365 7074 3a20 2a2f 2a0d 0a43 6f6e 7465  cept:.*/*..Conte
    0x00f0:  6e74 2d4c 656e 6774 683a 2032 350d 0a43  nt-Length:.25..C
    0x0100:  6f6e 7465 6e74 2d54 7970 653a 2061 7070  ontent-Type:.app
    0x0110:  6c69 6361 7469 6f6e 2f78 2d77 7777 2d66  lication/x-www-f
    0x0120:  6f72 6d2d 7572 6c65 6e63 6f64 6564 0d0a  orm-urlencoded..
    0x0130:  0d0a 6e61 6d65 3d72 6f6f 7426 7061 7373  ..name=root&pass
    0x0140:  776f 7264 3d68 6163 6b6d 65              word=hackme


Well, there it is. The traffic is not encrypted.

What we are going to do to include SSL support is to add a new server class that inherits from bottle.ServerAdapter and that will support SSL through cherrypy._cpwsgiserver3 included in python-cherrypy debian package instead of using cherrypy.wsgiserver provided by python-cherrypy3 debian package which is the supported one by bottle.

First of all, we will need to generate the .pem file. You can use this command:

 openssl req -new -x509 -keyout server.pem -out server.pem -days 365 -nodes


 I saved my server.pem file in /var/tmp/server.pem for this sample. Lets see the new code of our sample.


#!/usr/bin/env python
#-*- coding:utf-8 -*-


from bottle import Bottle, run, request, server_names, ServerAdapter

# Declaration of new class that inherits from ServerAdapter
# It's almost equal to the supported cherrypy class CherryPyServer
class MySSLCherryPy(ServerAdapter):
    def run(self, handler):
        from cherrypy import _cpwsgiserver3
        server = _cpwsgiserver3.CherryPyWSGIServer((self.host, self.port), handler)
    
        # If cert variable is has a valid path, SSL will be used
        # You can set it to None to disable SSL
        cert = '/var/tmp/server.pem' # certificate path 
        server.ssl_certificate = cert
        server.ssl_private_key = cert
        try:
            server.start()
        finally:
            server.stop()

# Add our new MySSLCherryPy class to the supported servers
# under the key 'mysslcherrypy'
server_names['mysslcherrypy'] = MySSLCherryPy

app = Bottle()

@app.post('/login')
def login():
    name = request.forms.get('name')
    password = request.forms.get('password')
    if (name, password) == ('root','hackme'):
        return "<p>Your login was correct</p>"
    else:
        return "<p>Login failed</p>"


# Add an additional parameter server='mysslcherrypy' so bottle
# will use our class
run(app, host='localhost', port='8080', server='mysslcherrypy')

As you can see, there is not a lot of code but, does it work?

As before, Terminal 1 is executing our application.

Terminal 2:

$ curl -d 'name=root&password=hackme' -k https://localhost:8080/login
<p>Your login was correct</p>

Note that we use -k to accept unverified certificates and, of course, https protocol.


Terminal 3:

# tcpdump -i lo dst port 8080 -XXX
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on lo, link-type EN10MB (Ethernet), capture size 65535 bytes
23:41:57.494848 IP localhost.38570 > localhost.http-alt: Flags [S], seq 3160714269, win 32792, options [mss 16396,sackOK,TS val 1454551 ecr 0,nop,wscale 4], length 0
    0x0000:  0000 0000 0000 0000 0000 0000 0800 4500  ..............E.
    0x0010:  003c 9dca 4000 4006 9eef 7f00 0001 7f00  .<..@.@.........
    0x0020:  0001 96aa 1f90 bc64 ac1d 0000 0000 a002  .......d........
    0x0030:  8018 fe30 0000 0204 400c 0402 080a 0016  ...0....@.......
    0x0040:  31d7 0000 0000 0103 0304                 1.........
23:41:57.494891 IP localhost.38570 > localhost.http-alt: Flags [.], ack 8931266, win 2050, options [nop,nop,TS val 1454551 ecr 1454551], length 0
    0x0000:  0000 0000 0000 0000 0000 0000 0800 4500  ..............E.
    0x0010:  0034 9dcb 4000 4006 9ef6 7f00 0001 7f00  .4..@.@.........
    0x0020:  0001 96aa 1f90 bc64 ac1e 0088 47c2 8010  .......d....G...
    0x0030:  0802 fe28 0000 0101 080a 0016 31d7 0016  ...(........1...
    0x0040:  31d7                                     1.
23:41:57.497047 IP localhost.38570 > localhost.http-alt: Flags [P.], seq 0:223, ack 1, win 2050, options [nop,nop,TS val 1454552 ecr 1454551], length 223
    0x0000:  0000 0000 0000 0000 0000 0000 0800 4500  ..............E.
    0x0010:  0113 9dcc 4000 4006 9e16 7f00 0001 7f00  ....@.@.........
    0x0020:  0001 96aa 1f90 bc64 ac1e 0088 47c2 8018  .......d....G...
    0x0030:  0802 ff07 0000 0101 080a 0016 31d8 0016  ............1...
    0x0040:  31d7 1603 0100 da01 0000 d603 014e de9a  1............N..
    0x0050:  35a9 ac95 ef29 5caf 4f47 a873 62fc 9cea  5....)\.OG.sb...
    0x0060:  9bda cacd 60aa ffa7 ca56 a00e 0800 005a  ....`....V.....Z
    0x0070:  c014 c00a 0039 0038 0088 0087 c00f c005  .....9.8........
    0x0080:  0035 0084 c012 c008 0016 0013 c00d c003  .5..............
    0x0090:  000a c013 c009 0033 0032 009a 0099 0045  .......3.2.....E
    0x00a0:  0044 c00e c004 002f 0096 0041 c011 c007  .D...../...A....
    0x00b0:  c00c c002 0005 0004 0015 0012 0009 0014  ................
    0x00c0:  0011 0008 0006 0003 00ff 0201 0000 5200  ..............R.
    0x00d0:  0000 0e00 0c00 0009 6c6f 6361 6c68 6f73  ........localhos
    0x00e0:  7400 0b00 0403 0001 0200 0a00 3400 3200  t...........4.2.
    0x00f0:  0100 0200 0300 0400 0500 0600 0700 0800  ................
    0x0100:  0900 0a00 0b00 0c00 0d00 0e00 0f00 1000  ................
    0x0110:  1100 1200 1300 1400 1500 1600 1700 1800  ................
    0x0120:  19                                       .
23:41:57.506522 IP localhost.38570 > localhost.http-alt: Flags [.], ack 801, win 2150, options [nop,nop,TS val 1454554 ecr 1454554], length 0
    0x0000:  0000 0000 0000 0000 0000 0000 0800 4500  ..............E.
    0x0010:  0034 9dcd 4000 4006 9ef4 7f00 0001 7f00  .4..@.@.........
    0x0020:  0001 96aa 1f90 bc64 acfd 0088 4ae2 8010  .......d....J...
    0x0030:  0866 fe28 0000 0101 080a 0016 31da 0016  .f.(........1...
    0x0040:  31da                                     1.
23:41:57.512658 IP localhost.38570 > localhost.http-alt: Flags [P.], seq 223:421, ack 801, win 2150, options [nop,nop,TS val 1454555 ecr 1454554], length 198
    0x0000:  0000 0000 0000 0000 0000 0000 0800 4500  ..............E.
    0x0010:  00fa 9dce 4000 4006 9e2d 7f00 0001 7f00  ....@.@..-......
    0x0020:  0001 96aa 1f90 bc64 acfd 0088 4ae2 8018  .......d....J...
    0x0030:  0866 feee 0000 0101 080a 0016 31db 0016  .f..........1...
    0x0040:  31da 1603 0100 8610 0000 8200 80ae d0f6  1...............
    0x0050:  bb61 7295 d39c 5af5 8bbe 70b5 d42c 7e37  .ar...Z...p..,~7
    0x0060:  5678 d1be 9641 f6b0 0dc5 d88b 599d d262  Vx...A......Y..b
    0x0070:  f0e6 39f0 a1a4 42c0 89af 50fb 4998 0685  ..9...B...P.I...
    0x0080:  f3db d028 6ab3 aa80 308a d592 debb 75ef  ...(j...0.....u.
    0x0090:  2097 017c 6205 3790 6ce6 c26b 4d1c 6ef7  ...|b.7.l..kM.n.
    0x00a0:  743e 3f94 2b75 aa67 4ff1 4330 9319 5960  t>?.+u.gO.C0..Y`
    0x00b0:  4087 5370 b8aa 5b67 5279 bf0a 8979 1a54  @.Sp..[gRy...y.T
    0x00c0:  2b92 37cf 199a a944 8ea0 0719 8f14 0301  +.7....D........
    0x00d0:  0001 0116 0301 0030 ffc7 337a b18b 7d92  .......0..3z..}.
    0x00e0:  6a9c 42ae cd5d 9afe 8104 c5f4 0181 c2d6  j.B..]..........
    0x00f0:  40ed 7c12 229b d34d 77e7 0dc5 6385 e486  @.|."..Mw...c...
    0x0100:  0a9b a10b d85c 03f2                      .....\..
                         

    [...]


As you can see, there is no clear text so it works!  Maybe this solution is a bit intrusive but... it is quick, easy and lightweight.  I don't know if there is a better solution but I would really like to know.

7 comments:

  1. Wow thanks a lot for writing this. I'm using CherryPy with Bottle and will be trying this out for some web services.

    ReplyDelete
  2. Thanks to you for your comment. You are the first one to post a comment here!! Hope this article had helped you. Regards.

    ReplyDelete
  3. Thanks for writing this! I, too, am using CherryPy with Bottle and was looking for exactly how to do this. This is very to-the-point and much appreciated.

    ReplyDelete
  4. Some corrections I had to make

    defined SSL key path
    key = /path/to/key
    server.ssl_private_key = key

    and

    from cherrypy import _cpwsgiserver3 threw errors

    REPLACED IT WITH THE FOLLOWING AND ALL WORKED!!


    from cherrypy import wsgiserver
    server = wsgiserver.CherryPyWSGIServer((self.host, self.port), handler)

    ReplyDelete
    Replies
    1. Yes, if you want to use the _cpwsgiserver3, you have to install the package. You are now using the standard cherrypy which is heavier but it is of course a good choice.

      Delete
  5. Replies
    1. Thanks a lot for the comment and welcome to the blog :).

      Delete