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?
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
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
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