Search code examples
pythonrestpublic-key-encryptionbottlewerkzeug

How to implement private/public apikey with werkzeug/bottle?


I am developing a RESTful web service with Bottle, probably soon to migrate to Werkzeug. I'd like to implement an auth scheme that works based on a private/public key pair where the server only has to store the public part while the user keeps the private one. Upon access, the server would require the accessor to perform an action using the private key that the server can verify and relate to the public key part. Upon success, for example a token is generated which can be used for some time. What is the path to implement something like this for Werkzeug or Bottle? Any projects/examples i could work from?


Solution

  • I believe, the best option here is to shift the responsibility to work with cryptography onto something else. Web servers and CA (certification authorities) are good with it.

    Basically, altogether they can

    1. ensure the connection is secure
    2. ensure the client uses certificate, signed by valid (say, yours) CA
    3. ensure the the certificate hasn't been revoked
    4. provide to your application the only information you need: the identifier of the remote party

    We use such mechanism to authenticate a third-party payment service (sorry, private code), and there is also a plugin to Redmine providing the same mechanism of client authorization, and we use it too (certainly, it's Ruby, but it's also a valid proof that such services can be found in wild.)

    To make things work, all you need is to

    1. configure your CA.
    2. configure your web-server to ensure it accepts certificates by your authority, and only these certificates, and passes the client id further to your app.
    3. ensure your application can extract client identifier from request.

    Below is an tiny example with easyrsa, nginx, uWsgi and werkzeug

    Configure CA

    The easyrsa toolkit is a part of OpenVPN installation. It's possible to use OpenSSL "raw" command or PyOpenSSL, but easyrsa is convenient and suitable at least at the concept stage.

    cp -a /usr/share/doc/openvpn/examples/easy-rsa/2.0 /etc/nginx/easyrsa
    cd /etc/nginx/easyrsa && source vars && ./clean-all
    

    Create CA

    ./build-ca
    

    Create server certificate

    ./build-key --server server
    

    Create client certificate.

    ./build-key-pkcs12  client1
    

    In the sample above you create both client secret key and its corresponding public part (certificate), but good practice assumes that you sign client certificate requests instead, and has no access to secret part.

    Some services generate a pair of secret key + certificate for you and then write a message on download page like "this is the only chance for you to download the secret key. We don't store it, so it cannot be download later."

    Additionally, this command creates a PKCS12 file with encrypted key and certificate, which is handy for import to browsers.

    Configure nginx

    First, we should create a pair "server certificate + ca certificate", as our CA self-signed:

    cat keys/server.crt keys/ca.crt > keys/server_and_ca.crt
    

    Then the following config can be applied:

    server {
        listen 443;
    
        location / {
            # Here we define the name and the contents of the WSGI variable to pass to service
            uwsgi_param SSL_CLIENT_ID $ssl_client_s_dn;
            include uwsgi_params;
            uwsgi_pass 127.0.0.1:5000;
        }
    
    
        # SSL support
        ssl                 on;
        ssl_protocols       SSLv3 TLSv1;
        ssl_certificate     easyrsa/keys/ca_and_server.crt;
        ssl_certificate_key easyrsa/keys/server.key;
    
        # We don't accept anyone without correct client certificate
        ssl_verify_client on;
        # The CA we use to verify client certificates
        ssl_client_certificate easyrsa/keys/ca.crt;
    }
    

    More information on configuration options is available here and here.

    Ensure you have correct privileges to the /etc/nginx/easyrsa directory and only root and nginx can get access to secret keys.

    Write a Werkzeug application

    The python part is trivial. Just read the variable SSL_CLIENT_ID from WSGI environment.

    The contents of the sample application in file sample.py

    from werkzeug.wrappers import Response
    
    def application(environ, start_response):
        text = 'Hello, your certificate id is %s\n' % environ.get('SSL_CLIENT_ID', '(unknown)')
        response = Response(text, mimetype='text/plain')
        return response(environ, start_response)
    

    Launch the service with a uwsgi server: uwsgi -w sample:application --socket 127.0.0.1:5000

    Test your installation

    It's easy to test with curl

    $ curl --cert keys/client1.crt --key keys/client1.key --cacert keys/ca.crt https://localhost/
    Hello, your certificate id is /C=US/ST=CA/L=SanFrancisco/O=Fort-Funston/OU=changeme/CN=client1/name=changeme/emailAddress=mail@host.domain