diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..b315ec4e6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.7-slim-stretch +WORKDIR /usr/src/app + +ENV CFSSL_SERVER wott-ca +ENV REDIS_SERVER redis + +RUN apt-get update && \ + apt-get install -y --no-install-recommends build-essential libssl-dev libffi-dev libltdl-dev && \ + apt-get clean + +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +COPY server.py ./ + +CMD FLASK_APP=server.py flask run --host 0.0.0.0 --port 5000 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..79e1bb1a6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +Flask==1.0.2 +cfssl==0.0.3b243 +redis==3.0.1 diff --git a/server.py b/server.py new file mode 100644 index 000000000..548a1c97a --- /dev/null +++ b/server.py @@ -0,0 +1,124 @@ +import os +import cfssl +import redis +import uuid +from flask import Flask, request, jsonify + +__author__ = "Viktor Petersson" +__version__ = "0.1.0" + +API_PREFIX = '/api/v0.1' +CFSSL_SERVER = os.getenv('CFSSL_SERVER', '127.0.0.1') +CFSSL_PORT = int(os.getenv('CFSSL_PORT', 8888)) + +app = Flask(__name__) +r = redis.Redis( + host=os.getenv('REDIS_SERVER', '127.0.0.1'), + port=int(os.getenv('REDIS_PORT', 6379)), + db=0 + ) + + +@app.route('/') +def root(): + return 'Nothing to see here. Go away!' + + +@app.route('/v0.1/ca', methods=['GET']) +def root_ca(): + """ + Returns the root certificate. + """ + # Try to use cache for cert retrieval + if r.get('ca_cert'): + return jsonify({ + 'ca': r.get('ca_cert').decode(), + }) + + print('Fetching root cert from CA...') + cf = cfssl.cfssl.CFSSL( + host=CFSSL_SERVER, + port=CFSSL_PORT, + ssl=False + ) + + ca = cf.info(label='primary')['certificate'] + r.set('ca_cert', ca) + return jsonify({'ca': ca}) + + +@app.route('/v0.1/generate-cert', methods=['GET']) +def generate_device_id(): + """ + Returns a random new device ID. + We're using Redis for this for now. + This needs to be moved to a proper database later. + + There's also a potential race condition here because + two devices could the same device_id before the CSR has + been signed and hence not locked. + """ + + cert_in_use = True + while cert_in_use: + device_id = '{}.d.wott.local'.format(uuid.uuid4().hex) + if not r.get(device_id): + cert_in_use = False + + return jsonify({'device_id': device_id}) + + +@app.route('/v0.1/cert-db/', methods=['GET']) +def get_device_cert(device_uuid): + """ + Retrieves the certificate for a given device. + """ + if r.get(device_uuid): + return jsonify({ + 'crt': r.get(device_uuid).decode(), + }) + else: + return 'Device not found.', 404 + + +@app.route('/v0.1/sign', methods=['POST']) +def sign_device_cert(): + """ + Signs a certificate. + """ + + content = request.get_json() + if not content: + return 'Invalid payload.', 400 + + if not content.get('csr'): + return 'Missing key "csr" in payload.', 400 + + if not content.get('device_id'): + return 'Missing key "device_id" in payload.', 400 + + # Basic check to only allow signing of certificates + # under the domain d.wott.io + if not content['device_id'].endswith('.d.wott.local'): + return 'Invalid device uuid', 400 + + # Only allow certificate to be signed once + if r.get(content['device_id']): + return 'Certificate already exist.', 400 + + cf = cfssl.cfssl.CFSSL( + host=CFSSL_SERVER, + port=CFSSL_PORT, + ssl=False + ) + + certificate = cf.sign( + certificate_request=content['csr'], + hosts=['{}'.format(content['device_id'])] + ) + + r.set(content['device_id'], certificate) + + return jsonify({ + 'crt': certificate, + })