Commit 5f6857b7 authored by Alain Takoudjou's avatar Alain Takoudjou

factorise cli_flask code, add more comments to functions, add admin revoke crt by serial

    Split cli_flask functions used to renew, sign and revoke certificate.
    Allow to revoke a certificate by serial PUT /crt/revoke/serial, this
    method required admin authentication. Also add GET /crt/serial/<string:serial>
parent 2d6b3bed
...@@ -158,6 +158,8 @@ class CertificateAuthority(object): ...@@ -158,6 +158,8 @@ class CertificateAuthority(object):
def getPendingCertificateRequest(self, csr_id): def getPendingCertificateRequest(self, csr_id):
""" """
Retrieve the content of a pending signing request. Retrieve the content of a pending signing request.
@param csr_id: The id of CSR returned by the storage
""" """
return self._storage.getPendingCertificateRequest(csr_id) return self._storage.getPendingCertificateRequest(csr_id)
...@@ -165,6 +167,8 @@ class CertificateAuthority(object): ...@@ -165,6 +167,8 @@ class CertificateAuthority(object):
""" """
Sanity-check CSR, stores it and generates a unique signing request Sanity-check CSR, stores it and generates a unique signing request
identifier (crt_id). identifier (crt_id).
@param csr: CSR string in PEM format
""" """
# Check number of already-pending signing requests # Check number of already-pending signing requests
# Check if csr is self-signed # Check if csr is self-signed
...@@ -191,12 +195,18 @@ class CertificateAuthority(object): ...@@ -191,12 +195,18 @@ class CertificateAuthority(object):
def deletePendingCertificateRequest(self, csr_id): def deletePendingCertificateRequest(self, csr_id):
""" """
Reject a pending certificate signing request. Reject a pending certificate signing request.
@param csr_id: The id of CSR returned by the storage
""" """
self._storage.deletePendingCertificateRequest(csr_id) self._storage.deletePendingCertificateRequest(csr_id)
def getPendingCertificateRequestList(self, limit=0, with_data=False): def getPendingCertificateRequestList(self, limit=0, with_data=False):
""" """
Return list of signed certificate Return list of pending certificate signature request
@param limit: number of element to fetch, 0 is not limit (int)
@param with_data: True or False, say if return csr PEM string associated
to others informations (bool).
""" """
return self._storage.getPendingCertificateRequestList(limit, with_data) return self._storage.getPendingCertificateRequestList(limit, with_data)
...@@ -204,6 +214,11 @@ class CertificateAuthority(object): ...@@ -204,6 +214,11 @@ class CertificateAuthority(object):
""" """
Generate new signed certificate. `ca_key_pair` is the CA key_pair to use Generate new signed certificate. `ca_key_pair` is the CA key_pair to use
if None, use the latest CA key_pair if None, use the latest CA key_pair
@param csr_id: CSR ID returned by storage, csr should be linked to the
new certificate (string).
@param ca_key_pair: The CA key_pair to used for signature. If None, the
latest key_pair is used.
""" """
# Apply extensions (ex: "not a certificate", ...) # Apply extensions (ex: "not a certificate", ...)
# Generate a certificate from the CSR # Generate a certificate from the CSR
...@@ -220,11 +235,33 @@ class CertificateAuthority(object): ...@@ -220,11 +235,33 @@ class CertificateAuthority(object):
return crt_id return crt_id
def getCertificate(self, crt_id): def getCertificate(self, crt_id):
"""
Return a Certificate string in PEM format
@param crt_id: Certificate ID returned by storage during certificate creation
"""
return self._storage.getCertificate(crt_id) return self._storage.getCertificate(crt_id)
def getCertificateFromSerial(self, serial):
"""
Return a Certificate string in PEM format
@param serial: serial of the certificate (string)
"""
cert = self._storage.getCertificateFromSerial(serial)
if not cert.content:
raise NotFound('Content certificate with serial %r is not found.' % (
serial,
))
return cert.content
def getSignedCertificateList(self, limit=0, with_data=False): def getSignedCertificateList(self, limit=0, with_data=False):
""" """
Return list of signed certificate Return list of signed certificate
@param limit: number of element to fetch, 0 is not limit (int)
@param with_data: True or False, say if return cert PEM string associated
to others informations (bool).
""" """
return self._storage.getSignedCertificateList(limit, with_data) return self._storage.getSignedCertificateList(limit, with_data)
...@@ -236,7 +273,7 @@ class CertificateAuthority(object): ...@@ -236,7 +273,7 @@ class CertificateAuthority(object):
def getValidCACertificateChain(self): def getValidCACertificateChain(self):
""" """
Return the ca certificate chain for all valid certificates Return the ca certificate chain for all valid certificates with key
""" """
result = [] result = []
iter_key_pair = iter(self._ca_key_pairs_list) iter_key_pair = iter(self._ca_key_pairs_list)
...@@ -251,6 +288,8 @@ class CertificateAuthority(object): ...@@ -251,6 +288,8 @@ class CertificateAuthority(object):
def getCAKeypairForCertificate(self, cert): def getCAKeypairForCertificate(self, cert):
""" """
Return the nearest CA key_pair to the next extension date of the cert Return the nearest CA key_pair to the next extension date of the cert
@param cert: X509 certificate
""" """
cert_valid_date = datetime.strptime(cert.get_notAfter(), '%Y%m%d%H%M%SZ') cert_valid_date = datetime.strptime(cert.get_notAfter(), '%Y%m%d%H%M%SZ')
next_valid_date = datetime.utcnow() + timedelta(0, self.crt_life_time) next_valid_date = datetime.utcnow() + timedelta(0, self.crt_life_time)
...@@ -274,6 +313,20 @@ class CertificateAuthority(object): ...@@ -274,6 +313,20 @@ class CertificateAuthority(object):
return selected_keypair return selected_keypair
def revokeCertificate(self, wrapped_crt): def revokeCertificate(self, wrapped_crt):
"""
Revoke a certificate
@param wrapped_crt: The revoke request dict containing certificate to
revoke and signature algorithm used to sign the request.
{
"signature": "signature string for payload",
"digest": "Signature algorithm (ex: SHA256"),
"payload": dict of data: {
"revoke_crt": "Certificate to revoke",
"reason": "Revoke reason"
}
}
"""
payload = utils.unwrap(wrapped_crt, lambda x: x['revoke_crt'], self.digest_list) payload = utils.unwrap(wrapped_crt, lambda x: x['revoke_crt'], self.digest_list)
try: try:
...@@ -287,18 +340,36 @@ class CertificateAuthority(object): ...@@ -287,18 +340,36 @@ class CertificateAuthority(object):
"The CA couldn't reconize the certificate to revoke.") "The CA couldn't reconize the certificate to revoke.")
crt = self._loadCertificate(payload['revoke_crt']) crt = self._loadCertificate(payload['revoke_crt'])
expiration_date = datetime.strptime(crt.get_notAfter(), '%Y%m%d%H%M%SZ')
expire_in = expiration_date - datetime.now()
if crt.has_expired():
raise ExpiredCertificate("Could not revoke a certificate which has expired" \
"since %r days." % -1*expire_in.days)
reason = payload['reason'] reason = payload['reason']
return self._storage.revokeCertificate( return self._storage.revokeCertificate(
utils.getSerialToInt(crt), utils.getSerialToInt(crt),
expiration_date,
reason) reason)
def revokeCertificateFromSerial(self, serial):
"""
Directly revoke a certificate from serial
@param serial: The serial of the certificate (int)
"""
return self._storage.revokeCertificate(
serial,
reason="")
def renew(self, wrapped_csr): def renew(self, wrapped_csr):
"""
Renew a certificate
@param wrapped_csr: The revoke request dict containing certificate to
revoke and signature algorithm used to sign the request.
{
"signature": "signature string for payload",
"digest": "Signature algorithm (ex: SHA256"),
"payload": dict of data: {
"crt": "Old certificate to renew",
"renew_csr": "New CSR to sign"
}
}
"""
payload = utils.unwrap(wrapped_csr, lambda x: x['crt'], self.digest_list) payload = utils.unwrap(wrapped_csr, lambda x: x['crt'], self.digest_list)
csr = payload['renew_csr'] csr = payload['renew_csr']
...@@ -377,8 +448,8 @@ class CertificateAuthority(object): ...@@ -377,8 +448,8 @@ class CertificateAuthority(object):
# Here comes the actual certificate # Here comes the actual certificate
serial = self._storage.getNextCertificateSerialNumber() serial = self._storage.getNextCertificateSerialNumber()
cert = crypto.X509() cert = crypto.X509()
# 3 = v3 # version v3
cert.set_version(3) cert.set_version(2)
cert.set_serial_number(serial) cert.set_serial_number(serial)
cert.gmtime_adj_notBefore(0) cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notAfter(self.crt_life_time) cert.gmtime_adj_notAfter(self.crt_life_time)
......
This diff is collapsed.
...@@ -24,7 +24,7 @@ from datetime import datetime, timedelta ...@@ -24,7 +24,7 @@ from datetime import datetime, timedelta
from OpenSSL import crypto from OpenSSL import crypto
from caucase import db from caucase import db
from caucase import utils from caucase import utils
from caucase.exceptions import (NoStorage, NotFound, Found) from caucase.exceptions import (NoStorage, NotFound, Found, ExpiredCertificate)
from flask_user import UserMixin from flask_user import UserMixin
STATUS_VALIDATED = 'validated' STATUS_VALIDATED = 'validated'
...@@ -326,7 +326,7 @@ class Storage(object): ...@@ -326,7 +326,7 @@ class Storage(object):
return data_list return data_list
def revokeCertificate(self, serial, not_after_date, reason=''): def revokeCertificate(self, serial, reason=''):
""" """
Add serial to the list of revoked certificates. Add serial to the list of revoked certificates.
Associated certificate must expire at (or before) not_after_date, so Associated certificate must expire at (or before) not_after_date, so
...@@ -341,11 +341,16 @@ class Storage(object): ...@@ -341,11 +341,16 @@ class Storage(object):
if not cert: if not cert:
raise NotFound('No certficate with serial %r' % (serial, )) raise NotFound('No certficate with serial %r' % (serial, ))
expire_in = cert.expire_after - datetime.utcnow()
if expire_in.days < 0:
raise ExpiredCertificate("Certificate with serial %r has expired" \
" since %r day(s)." % (serial, -1*expire_in.days))
revoke = Revocation( revoke = Revocation(
serial=serial, serial=serial,
creation_date=datetime.utcnow(), creation_date=datetime.utcnow(),
reason=reason, reason=reason,
crt_expire_after=not_after_date crt_expire_after=cert.expire_after
) )
# Set latest CRL as expired, it will be regenerated # Set latest CRL as expired, it will be regenerated
crl = CertificateRevocationList.query.filter( crl = CertificateRevocationList.query.filter(
......
...@@ -27,7 +27,6 @@ from caucase.web import parseArguments, configure_flask ...@@ -27,7 +27,6 @@ from caucase.web import parseArguments, configure_flask
from OpenSSL import crypto, SSL from OpenSSL import crypto, SSL
from caucase.exceptions import (NoStorage, NotFound, Found) from caucase.exceptions import (NoStorage, NotFound, Found)
from caucase import utils from caucase import utils
from caucase import db, app
from flask_testing import TestCase from flask_testing import TestCase
from flask import url_for from flask import url_for
...@@ -38,14 +37,16 @@ class CertificateAuthorityWebTest(TestCase): ...@@ -38,14 +37,16 @@ class CertificateAuthorityWebTest(TestCase):
configure_flask(parseArguments(['--ca-dir', self.ca_dir, '-s', '/CN=CA Auth Test/emailAddress=xx@example.com'])) configure_flask(parseArguments(['--ca-dir', self.ca_dir, '-s', '/CN=CA Auth Test/emailAddress=xx@example.com']))
def tearDown(self): def tearDown(self):
db.session.remove() self.db.session.remove()
db.drop_all() self.db.drop_all()
if os.path.exists(self.ca_dir): if os.path.exists(self.ca_dir):
shutil.rmtree(self.ca_dir) shutil.rmtree(self.ca_dir)
def create_app(self): def create_app(self):
from caucase import db, app
app.config['TESTING'] = True app.config['TESTING'] = True
app.config['LIVESERVER_PORT'] = 0 app.config['LIVESERVER_PORT'] = 0
self.db = db
return app return app
def generateCSR(self, cn="toto.example.com"): def generateCSR(self, cn="toto.example.com"):
......
...@@ -38,8 +38,7 @@ from caucase.exceptions import (NoStorage, NotFound, Found, BadSignature, ...@@ -38,8 +38,7 @@ from caucase.exceptions import (NoStorage, NotFound, Found, BadSignature,
CertificateVerificationError, CertificateVerificationError,
ExpiredCertificate) ExpiredCertificate)
from functools import wraps from functools import wraps
from caucase import utils from caucase import utils, app, db
from caucase import app, db
class DisabledStringField(StringField): class DisabledStringField(StringField):
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
...@@ -191,7 +190,6 @@ def configure_flask(options): ...@@ -191,7 +190,6 @@ def configure_flask(options):
app.logger.addHandler(logger) app.logger.addHandler(logger)
# Instanciate storage # Instanciate storage
# XXX - check loaded_crt_life_time here
from caucase.storage import Storage from caucase.storage import Storage
storage = Storage(db, storage = Storage(db,
max_csr_amount=options.max_request_amount, max_csr_amount=options.max_request_amount,
...@@ -221,7 +219,7 @@ def configure_flask(options): ...@@ -221,7 +219,7 @@ def configure_flask(options):
def check_authentication(username, password): def check_authentication(username, password):
user = app.config.storage.findUser(username) user = app.config.storage.findUser(username)
if user: if user:
return app.user_manager.hash_password(password) == user.password return app.user_manager.verify_password(password, user)
else: else:
return False return False
...@@ -234,7 +232,7 @@ def authenticated_method(func): ...@@ -234,7 +232,7 @@ def authenticated_method(func):
auth = request.authorization auth = request.authorization
if not auth: if not auth:
return abort(401) return abort(401)
elif not Users.check_authentication(auth.username, auth.password): elif not check_authentication(auth.username, auth.password):
return abort(401) return abort(401)
# Call the actual view # Call the actual view
return func(*args, **kwargs) return func(*args, **kwargs)
...@@ -360,12 +358,18 @@ def before_request(): ...@@ -360,12 +358,18 @@ def before_request():
@app.route('/crl', methods=['GET']) @app.route('/crl', methods=['GET'])
def get_crl(): def get_crl():
"""
Get the lastest CRL (certificate revocation list)
"""
crl_content = app.config.ca.getCertificateRevocationList() crl_content = app.config.ca.getCertificateRevocationList()
return send_file_content(crl_content, 'ca.crl.pem') return send_file_content(crl_content, 'ca.crl.pem')
@app.route('/csr/<string:csr_id>', methods=['GET']) @app.route('/csr/<string:csr_id>', methods=['GET'])
def get_csr(csr_id): def get_csr(csr_id):
"""
Get a CSR string in PEM format from identified by `csr_id`.
"""
try: try:
csr_content = app.config.ca.getPendingCertificateRequest(csr_id) csr_content = app.config.ca.getPendingCertificateRequest(csr_id)
...@@ -377,6 +381,9 @@ def get_csr(csr_id): ...@@ -377,6 +381,9 @@ def get_csr(csr_id):
@app.route('/csr', methods=['PUT']) @app.route('/csr', methods=['PUT'])
def request_cert(): def request_cert():
"""
Store certificate signature request (csr) in PEM format
"""
csr_content = request.form.get('csr', '').encode('utf-8') csr_content = request.form.get('csr', '').encode('utf-8')
if not csr_content: if not csr_content:
raise FlaskException("'csr' parameter is mandatory", raise FlaskException("'csr' parameter is mandatory",
...@@ -399,6 +406,9 @@ def request_cert(): ...@@ -399,6 +406,9 @@ def request_cert():
@app.route('/csr/<string:csr_id>', methods=['DELETE']) @app.route('/csr/<string:csr_id>', methods=['DELETE'])
@authenticated_method @authenticated_method
def remove_csr(csr_id): def remove_csr(csr_id):
"""
Delete a Certificate signature request. Authentication required
"""
try: try:
app.config.ca.deletePendingCertificateRequest(csr_id) app.config.ca.deletePendingCertificateRequest(csr_id)
...@@ -412,6 +422,9 @@ def remove_csr(csr_id): ...@@ -412,6 +422,9 @@ def remove_csr(csr_id):
@app.route('/crt/<string:cert_id>', methods=['GET']) @app.route('/crt/<string:cert_id>', methods=['GET'])
def get_crt(cert_id): def get_crt(cert_id):
"""
Get a certificate by the id `cert_id`
"""
try: try:
cert_content = app.config.ca.getCertificate(cert_id) cert_content = app.config.ca.getCertificate(cert_id)
...@@ -421,8 +434,24 @@ def get_crt(cert_id): ...@@ -421,8 +434,24 @@ def get_crt(cert_id):
return send_file_content(cert_content, cert_id) return send_file_content(cert_content, cert_id)
@app.route('/crt/serial/<string:serial>', methods=['GET'])
def crt_fromserial(serial):
"""
Get a certificate by the serial
"""
try:
cert_content = app.config.ca.getCertificateFromSerial(serial)
except NotFound, e:
raise FlaskException("%s" % str(e),
status_code=404, payload={"name": "FileNotFound", "code": 1})
return send_file_content(cert_content, '%s.crt.pem' % serial)
@app.route('/crt/ca.crt.pem', methods=['GET']) @app.route('/crt/ca.crt.pem', methods=['GET'])
def get_cacert(): def get_cacert():
"""
Get CA Certificate in PEM format string.
"""
ca_cert = app.config.ca.getCACertificate() ca_cert = app.config.ca.getCACertificate()
...@@ -430,7 +459,11 @@ def get_cacert(): ...@@ -430,7 +459,11 @@ def get_cacert():
@app.route('/crt/ca.crt.json', methods=['GET']) @app.route('/crt/ca.crt.json', methods=['GET'])
def get_cacert_json(): def get_cacert_json():
"""
Return CA certificate chain list, if the CA certificate is being renewed
the list will contain the next certificate and the old certificate which
will expire soon.
"""
ca_chain_list = app.config.ca.getValidCACertificateChain() ca_chain_list = app.config.ca.getValidCACertificateChain()
return jsonify(ca_chain_list) return jsonify(ca_chain_list)
...@@ -460,6 +493,9 @@ def signcert(csr_key, redirect_to=''): ...@@ -460,6 +493,9 @@ def signcert(csr_key, redirect_to=''):
@app.route('/crt', methods=['PUT']) @app.route('/crt', methods=['PUT'])
@authenticated_method @authenticated_method
def sign_cert(): def sign_cert():
"""
Sign a certificate, require authentication
"""
key = request.form.get('csr_id', '').encode('utf-8') key = request.form.get('csr_id', '').encode('utf-8')
if not key: if not key:
raise FlaskException("'csr_id' parameter is a mandatory parameter", raise FlaskException("'csr_id' parameter is a mandatory parameter",
...@@ -570,9 +606,30 @@ def request_revoke_crt(): ...@@ -570,9 +606,30 @@ def request_revoke_crt():
response = Response("", status=201, ) response = Response("", status=201, )
return response return response
@app.route('/crt/revoke/serial', methods=['PUT'])
@authenticated_method
def revoke_crt():
"""
Directly revoke a certificate from his serial
"""
try:
serial = request.form.get('serial', '')
app.config.ca.revokeCertificateFromSerial(serial)
except ValueError, e:
traceback.print_exc()
raise FlaskException(str(e),
payload={"name": "FileFormat", "code": 3})
except ExpiredCertificate, e:
raise FlaskException(str(e),
payload={"name": "FileFormat", "code": 3})
except NotFound, e:
raise FlaskException(str(e),
status_code=404, payload={"name": "FileNotFound", "code": 1})
response = Response("", status=201)
return response
#Manage routes (Authentication required) - Flask APP #Manage routes (Authentication required) - Flask APP
......
...@@ -18,7 +18,8 @@ ...@@ -18,7 +18,8 @@
import os import os
from caucase.web import parseArguments, configure_flask, app from caucase import app
from caucase.web import parseArguments, configure_flask
from werkzeug.contrib.fixers import ProxyFix from werkzeug.contrib.fixers import ProxyFix
def readConfigFromFile(config_file): def readConfigFromFile(config_file):
...@@ -52,4 +53,4 @@ def start_wsgi(): ...@@ -52,4 +53,4 @@ def start_wsgi():
app.logger.info("Certificate Authority server ready...") app.logger.info("Certificate Authority server ready...")
if __name__ == 'caucase.wsgi': if __name__ == 'caucase.wsgi':
start_wsgi() start_wsgi()
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment