Commit ea435ee7 authored by Alain Takoudjou's avatar Alain Takoudjou

Improve certificate authority, allow to set password at first access and Add...

Improve certificate authority, allow to set password at first access and Add initials web services for administration

Certificate Authority web use a database (see models.py) to store some informations about certificate. This help
to not access singles files too much and also help to keep trace on what is done.
The admin can revoke any certificate at any moment, users can also send revocation request to CA, administrator
will decide to apply request or reject it.
Admin can also cancel a CSR (this will delete csr file).
parent 07555096
...@@ -32,6 +32,7 @@ setup(name=name, ...@@ -32,6 +32,7 @@ setup(name=name,
include_package_data=True, include_package_data=True,
install_requires=[ install_requires=[
'Flask', # needed by servers 'Flask', # needed by servers
'flask_user',
'atomize', # needed by pubsub 'atomize', # needed by pubsub
'feedparser', # needed by pubsub 'feedparser', # needed by pubsub
'apache_libcloud>=0.4.0', # needed by cloudmgr 'apache_libcloud>=0.4.0', # needed by cloudmgr
......
...@@ -29,6 +29,10 @@ def popenCommunicate(command_list, input=None): ...@@ -29,6 +29,10 @@ def popenCommunicate(command_list, input=None):
command_list, result)) command_list, result))
return result return result
class BusyCertificateAuthority(Exception):
"""Exception raised when certificate authority is busy"""
pass
class GeneralNames(rfc2459.GeneralNames): class GeneralNames(rfc2459.GeneralNames):
""" """
rfc2459 has wrong tagset. rfc2459 has wrong tagset.
...@@ -56,7 +60,7 @@ class ExtensionType(): ...@@ -56,7 +60,7 @@ class ExtensionType():
NS_COMMENT = "nsComment" NS_COMMENT = "nsComment"
SUBJECT_KEY_ID = "subjectKeyIdentifier" SUBJECT_KEY_ID = "subjectKeyIdentifier"
AUTH_KEY_ID = "authorityKeyIdentifier" AUTH_KEY_ID = "authorityKeyIdentifier"
class X509Extension(object): class X509Extension(object):
...@@ -201,7 +205,7 @@ class CertificateBase(object): ...@@ -201,7 +205,7 @@ class CertificateBase(object):
try: try:
key_fd = os.open(output_file, key_fd = os.open(output_file,
os.O_CREAT|os.O_WRONLY|os.O_EXCL|os.O_TRUNC, os.O_CREAT|os.O_WRONLY|os.O_EXCL|os.O_TRUNC,
0640) 0600)
except OSError, e: except OSError, e:
if e.errno != errno.EEXIST: if e.errno != errno.EEXIST:
raise raise
...@@ -243,6 +247,7 @@ class CertificateBase(object): ...@@ -243,6 +247,7 @@ class CertificateBase(object):
# XXX Considering only one trusted certificate here # XXX Considering only one trusted certificate here
if not self.verifyCertificateChain(cert, [ca_cert]): if not self.verifyCertificateChain(cert, [ca_cert]):
return False return False
return False
if key_file: if key_file:
return self.validateCertAndKey(cert_file, key_file) return self.validateCertAndKey(cert_file, key_file)
return True return True
...@@ -265,6 +270,13 @@ class CertificateBase(object): ...@@ -265,6 +270,13 @@ class CertificateBase(object):
x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert) x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
return x509 return x509
def freadCrl(self, crl_file):
with open(crl_file) as f_crl:
return self.readCrl(f_crl.read())
def readCrl(self, crl):
return crypto.load_crl(crypto.FILETYPE_PEM, crl)
def readPkey(self, key): def readPkey(self, key):
return crypto.load_privatekey(crypto.FILETYPE_PEM, key) return crypto.load_privatekey(crypto.FILETYPE_PEM, key)
...@@ -277,6 +289,19 @@ class CertificateBase(object): ...@@ -277,6 +289,19 @@ class CertificateBase(object):
subject_dict = dict(subject.get_components()) subject_dict = dict(subject.get_components())
return subject_dict return subject_dict
def getX509Text(self, openssl_binary, cert_file):
# XXX - using openssl_bin to get text information from certificate
return popenCommunicate([openssl_binary, 'x509', '-in', cert_file, '-text'])
def getCsrText(self, openssl_binary, csr_file):
# XXX - using openssl_bin to get text information from certificate
return popenCommunicate([openssl_binary, 'req', '-in', csr_file, '-text'])
def getCrlText(self, openssl_binary, crl_file):
# XXX - using openssl_bin to get text information from certificate
return popenCommunicate([openssl_binary, 'crl', '-in', crl_file, '-text'])
class CertificateAuthority(CertificateBase): class CertificateAuthority(CertificateBase):
def __init__(self, openssl_binary, def __init__(self, openssl_binary,
...@@ -284,8 +309,6 @@ class CertificateAuthority(CertificateBase): ...@@ -284,8 +309,6 @@ class CertificateAuthority(CertificateBase):
key='cakey.pem', crl='cacrl.pem', ca_directory=None): key='cakey.pem', crl='cacrl.pem', ca_directory=None):
self.key = key self.key = key
self.certificate = certificate self.certificate = certificate
# XXX - this class is still using openssl_bin when changed, X509Extension should be used in generated and signed certs
# self.X509Extension = X509Extension()
self.openssl_binary = openssl_binary self.openssl_binary = openssl_binary
self.openssl_configuration = openssl_configuration self.openssl_configuration = openssl_configuration
self.ca_directory = ca_directory self.ca_directory = ca_directory
...@@ -293,6 +316,7 @@ class CertificateAuthority(CertificateBase): ...@@ -293,6 +316,7 @@ class CertificateAuthority(CertificateBase):
self.ca_directory = os.getcwd() self.ca_directory = os.getcwd()
self.ca_crl = crl self.ca_crl = crl
self.lock_file = os.path.join(self.ca_directory, 'ca_locked')
for file in ['crlnumber', 'serial']: for file in ['crlnumber', 'serial']:
if not os.path.exists(os.path.join(self.ca_directory, file)): if not os.path.exists(os.path.join(self.ca_directory, file)):
...@@ -303,6 +327,21 @@ class CertificateAuthority(CertificateBase): ...@@ -303,6 +327,21 @@ class CertificateAuthority(CertificateBase):
with open(os.path.join(self.ca_directory, 'index.txt'), 'w') as f: with open(os.path.join(self.ca_directory, 'index.txt'), 'w') as f:
f.write('') f.write('')
def _lock(self):
"""Checks lock and locks Certificate Authority tool
Raises BusyCertificateAuthority"""
if os.path.exists(self.lock_file):
raise BusyCertificateAuthority
open(self.lock_file, 'w').write('locked')
def _unlock(self):
"""Checks lock and locks Certificate Authority tool"""
if os.path.exists(self.lock_file):
os.unlink(self.lock_file)
else:
pass
def checkAuthority(self): def checkAuthority(self):
if self.openssl_configuration is None: if self.openssl_configuration is None:
raise ValueError('Openssl configuration file not found!') raise ValueError('Openssl configuration file not found!')
...@@ -338,8 +377,6 @@ class CertificateAuthority(CertificateBase): ...@@ -338,8 +377,6 @@ class CertificateAuthority(CertificateBase):
if self.openssl_configuration is None: if self.openssl_configuration is None:
raise ValueError('Openssl configuration file not found!') raise ValueError('Openssl configuration file not found!')
# XXX - self.X509Extension.setDefaultExtensions(cert_obj, subject=None, issuer=None, crl_url=http://ca.url/cacrl.pem)
# XXX - for now, crlDistributionPoints should be in openssl.cnf file
popenCommunicate([self.openssl_binary, 'ca', '-batch', '-config', popenCommunicate([self.openssl_binary, 'ca', '-batch', '-config',
self.openssl_configuration, '-out', output_file, self.openssl_configuration, '-out', output_file,
'-infiles', csr_file]) '-infiles', csr_file])
...@@ -349,15 +386,24 @@ class CertificateAuthority(CertificateBase): ...@@ -349,15 +386,24 @@ class CertificateAuthority(CertificateBase):
raise ValueError('Openssl configuration file not found!') raise ValueError('Openssl configuration file not found!')
popenCommunicate([self.openssl_binary, 'ca', '-batch', '-config', popenCommunicate([self.openssl_binary, 'ca', '-batch', '-config',
self.openssl_configuration, '-gencrl', '-out', self.ca_crl]) self.openssl_configuration, '-gencrl', '-keyfile', self.key,
'-cert', self.certificate, '-out', self.ca_crl])
def revokeCertificate(self, cert_file): def revokeCertificate(self, cert_file, update_crl=False):
if self.openssl_configuration is None: if self.openssl_configuration is None:
raise ValueError('Openssl configuration file not found!') raise ValueError('Openssl configuration file not found!')
popenCommunicate([self.openssl_binary, 'ca', '-batch', '-config', popenCommunicate([self.openssl_binary, 'ca', '-batch', '-config',
self.openssl_configuration, '-revoke', cert_file]) self.openssl_configuration, '-revoke', cert_file])
if update_crl:
self.genCertificateRevocationList()
def getNextSerial(self):
# XXX - serial is managed by openssl
return open(os.path.join(self.ca_directory, 'serial')).read().strip()
class CertificateAuthorityRequest(CertificateBase): class CertificateAuthorityRequest(CertificateBase):
def __init__(self, key, certificate, cacertificate, ca_url, def __init__(self, key, certificate, cacertificate, ca_url,
...@@ -369,7 +415,6 @@ class CertificateAuthorityRequest(CertificateBase): ...@@ -369,7 +415,6 @@ class CertificateAuthorityRequest(CertificateBase):
self.ca_url = ca_url self.ca_url = ca_url
self.logger = logger self.logger = logger
self.max_retry = max_retry self.max_retry = max_retry
self.X509Extension = X509Extension()
if self.logger is None: if self.logger is None:
self.logger = logging.getLogger('Certificate Request') self.logger = logging.getLogger('Certificate Request')
...@@ -424,16 +469,13 @@ class CertificateAuthorityRequest(CertificateBase): ...@@ -424,16 +469,13 @@ class CertificateAuthorityRequest(CertificateBase):
finally: finally:
os.close(fd) os.close(fd)
def signCertificateWeb(self, csr_file): def signCertificateWeb(self, csr_file, auto_revoke=False):
if os.path.exists(self.certificate) and os.stat(self.certificate).st_size > 0: if os.path.exists(self.certificate) and os.stat(self.certificate).st_size > 0:
return return
with open(csr_file) as fcsr: with open(csr_file) as fcsr:
csr = fcsr.read() csr = fcsr.read()
#req = self.freadCertificateRequest(csr_file)
#common_name = self.getCN(req)
data = {'csr': csr} data = {'csr': csr}
retry = 0 retry = 0
sleep_time = 10 sleep_time = 10
...@@ -454,7 +496,7 @@ class CertificateAuthorityRequest(CertificateBase): ...@@ -454,7 +496,7 @@ class CertificateAuthorityRequest(CertificateBase):
response = self._request('post', request_url, data=data) response = self._request('post', request_url, data=data)
while (not response or response.status_code != 200) and retry < self.max_retry: while (not response or response.status_code != 200) and retry < self.max_retry:
self.logger.error("%s: Failed to send CSR. \n%s" % ( self.logger.error("%s: Failed to send CSR. \n%s" % (
response.status_code, response.text)) response.status_code, response.text))
self.logger.info("will retry in %s seconds..." % sleep_time) self.logger.info("will retry in %s seconds..." % sleep_time)
...@@ -463,8 +505,8 @@ class CertificateAuthorityRequest(CertificateBase): ...@@ -463,8 +505,8 @@ class CertificateAuthorityRequest(CertificateBase):
response = self._request('post', request_url, data=data) response = self._request('post', request_url, data=data)
if response.status_code != 200: if response.status_code != 200:
raise Exception("ERROR: failed to post CSR after % retry. Exiting..." % max_retry) raise Exception("ERROR: failed to post CSR after % retry. Exiting..." % retry)
self.logger.info("CSR succefully sent.") self.logger.info("CSR succefully sent.")
self.logger.debug("Server reponse with csr key is %s" % response.text) self.logger.debug("Server reponse with csr key is %s" % response.text)
csr_key = response.text csr_key = response.text
...@@ -493,6 +535,50 @@ class CertificateAuthorityRequest(CertificateBase): ...@@ -493,6 +535,50 @@ class CertificateAuthorityRequest(CertificateBase):
os.close(fd) os.close(fd)
os.unlink(cert_temp) os.unlink(cert_temp)
else: else:
if auto_revoke:
self.logger.error("Certificate validation failed. " \
"The signed certificate is going to be revoked...")
self.revokeCertificateRequest(cert_temp,
csr_key,
message="Certificate validation failed.")
# Cleanup
try:
os.remove(csr_key_file)
except OSError, e:
if e.errno != errno.ENOENT:
# raise
pass
raise Exception("Error: Certificate validation failed. " \ raise Exception("Error: Certificate validation failed. " \
"This signed certificate should be revoked!") "This signed certificate should be revoked!")
self.logger.info("Certificate correctly saved at %s." % self.certificate) self.logger.info("Certificate correctly saved at %s." % self.certificate)
def revokeCertificateRequest(self, cert_file, key_name, message=""):
"""
Send a revocation request for the givent certificate to the master.
"""
sleep_time = 10
retry = 0
cert = self.freadX509(cert_file)
serial = '{0:x}'.format(int(cert.get_serial_number()))
request_url = '%s/requestrevoke' % self.ca_url
data = {'serial': serial, 'name': key_name, 'reason': message}
self.logger.info("Sent Certificate revocation request for %s, serial=%s." % (
key_name, serial))
response = self._request('post', request_url, data=data)
while (not response or response.status_code != 200) and retry < self.max_retry:
self.logger.error("%s: Failed to send Rovocation request. \n%s" % (
response.status_code, response.text))
self.logger.info("will retry in %s seconds..." % sleep_time)
time.sleep(sleep_time)
retry += 1
response = self._request('post', request_url, data=data)
if response.status_code != 200:
raise Exception("ERROR: failed to post revoke request after %s retry. Exiting..." % retry)
self.logger.info("Certificate revocation request for %s.cert.pem successfully sent." % (
key_name))
...@@ -13,7 +13,7 @@ from slapos.certificate_authority.certificate_authority import CertificateAuthor ...@@ -13,7 +13,7 @@ from slapos.certificate_authority.certificate_authority import CertificateAuthor
def parseArguments(): def parseArguments():
""" """
Parse arguments for monitor instance. Parse arguments for Certificate Authority Request.
""" """
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('--cert_file', parser.add_argument('--cert_file',
...@@ -50,6 +50,9 @@ def parseArguments(): ...@@ -50,6 +50,9 @@ def parseArguments():
parser.add_argument('--organization_unit', parser.add_argument('--organization_unit',
default='Company Unit', default='Company Unit',
help='The Organisation Unit Name') help='The Organisation Unit Name')
parser.add_argument('--auto_revoke',
default=True, action="store_true",
help='Request Revoke Certificate if validation fail')
return parser return parser
...@@ -83,5 +86,5 @@ def requestCertificateWeb(): ...@@ -83,5 +86,5 @@ def requestCertificateWeb():
organization=config.organization, organization=config.organization,
organization_unit=config.organization_unit, digest="sha1") organization_unit=config.organization_unit, digest="sha1")
ca.signCertificateWeb(config.csr_file) ca.signCertificateWeb(config.csr_file, auto_revoke=config.auto_revoke)
...@@ -10,12 +10,15 @@ import logging.handlers ...@@ -10,12 +10,15 @@ import logging.handlers
import os, errno import os, errno
import argparse import argparse
import traceback import traceback
from slapos.certificate_authority.web import app from flask_user import UserManager, SQLAlchemyAdapter
from flask_mail import Mail
from slapos.certificate_authority.web.views import app
from slapos.certificate_authority.web.start_web import app, db, init_app
from slapos.certificate_authority.certificate_authority import CertificateAuthority from slapos.certificate_authority.certificate_authority import CertificateAuthority
def parseArguments(): def parseArguments():
""" """
Parse arguments for monitor instance. Parse arguments for Certificate Authority instance.
""" """
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('--ca_dir', parser.add_argument('--ca_dir',
...@@ -44,10 +47,12 @@ def parseArguments(): ...@@ -44,10 +47,12 @@ def parseArguments():
default='9086', default='9086',
help='Port for ca server.') help='Port for ca server.')
parser.add_argument('--debug', parser.add_argument('--debug',
action="store_true", dest="debug", action="store_true", dest="debug", default=True,
help='Enable debug mode.') help='Enable debug mode.')
parser.add_argument('--log_file', parser.add_argument('--log_file',
help='Path for log output') help='Path for log output')
parser.add_argument('--db_file',
help='Path of file to use to store User Account information. Default: $ca_dir/ca.db')
parser.add_argument('--trusted_host', parser.add_argument('--trusted_host',
default=[], default=[],
action='append', dest='trusted_host_list', action='append', dest='trusted_host_list',
...@@ -93,6 +98,8 @@ def start(): ...@@ -93,6 +98,8 @@ def start():
options.ca_dir = os.path.abspath(options.ca_dir) options.ca_dir = os.path.abspath(options.ca_dir)
if not options.config_file: if not options.config_file:
options.config_file = os.path.join(options.ca_dir, 'openssl.cnf') options.config_file = os.path.join(options.ca_dir, 'openssl.cnf')
if not options.db_file:
options.db_file = os.path.join(options.ca_dir, 'ca.db')
if not options.certs_dir: if not options.certs_dir:
options.certs_dir = os.path.join(options.ca_dir, 'certs') options.certs_dir = os.path.join(options.ca_dir, 'certs')
if not options.key_dir: if not options.key_dir:
...@@ -106,12 +113,14 @@ def start(): ...@@ -106,12 +113,14 @@ def start():
if not options.crl_file: if not options.crl_file:
options.crl_file = os.path.join(options.crl_dir, 'cacrl.pem') options.crl_file = os.path.join(options.crl_dir, 'cacrl.pem')
os.chdir(options.ca_dir)
logger = getLogger(options.debug, options.log_file) logger = getLogger(options.debug, options.log_file)
ca = CertificateAuthority(options.openssl_bin, ca = CertificateAuthority(options.openssl_bin,
openssl_configuration=options.config_file, certificate=options.cert_file, openssl_configuration=options.config_file, certificate=options.cert_file,
key=options.key_file, crl=options.crl_file, ca_directory=options.ca_dir) key=options.key_file, crl=options.crl_file, ca_directory=options.ca_dir)
#config = Config() app.config.from_object('slapos.certificate_authority.web.settings')
app.config.update( app.config.update(
ca_dir=options.ca_dir, ca_dir=options.ca_dir,
trusted_host_list=options.trusted_host_list, trusted_host_list=options.trusted_host_list,
...@@ -121,7 +130,10 @@ def start(): ...@@ -121,7 +130,10 @@ def start():
newcert_dir=os.path.join(options.ca_dir, 'newcerts'), newcert_dir=os.path.join(options.ca_dir, 'newcerts'),
crl_dir=os.path.join(options.ca_dir, 'crl'), crl_dir=os.path.join(options.ca_dir, 'crl'),
key_dir=os.path.join(options.ca_dir, 'private'), key_dir=os.path.join(options.ca_dir, 'private'),
db_file=options.db_file,
SQLALCHEMY_DATABASE_URI='sqlite:///%s' % options.db_file,
ca=ca, ca=ca,
log_file=options.log_file,
) )
for key in ['csr', 'req', 'cert', 'crl', 'key', 'newcert']: for key in ['csr', 'req', 'cert', 'crl', 'key', 'newcert']:
...@@ -136,14 +148,17 @@ def start(): ...@@ -136,14 +148,17 @@ def start():
# Generate certificate Authority cert and key # Generate certificate Authority cert and key
ca.checkAuthority() ca.checkAuthority()
# XXX - maybe CRL must be generated from cron every xx hours if not os.path.exists(options.crl_file):
ca.genCertificateRevocationList() # XXX - maybe CRL must be generated from cron every xx hours
ca.genCertificateRevocationList()
# Initialize Flask extensions
init_app()
app.logger.addHandler(logger) app.logger.addHandler(logger)
app.logger.info("Certificate Authority server started on http://%s:%s" % ( app.logger.info("Certificate Authority server started on http://%s:%s" % (
options.host, options.port)) options.host, options.port))
app.run( app.run(
debug=options.debug, # debug=options.debug,
host=options.host, host=options.host,
port=int(options.port) port=int(options.port)
) )
...@@ -15,7 +15,7 @@ CLIENT_IP = '::1' ...@@ -15,7 +15,7 @@ CLIENT_IP = '::1'
def parseArguments(): def parseArguments():
""" """
Parse arguments for monitor instance. Parse arguments for Certificate Authority Manage.
""" """
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('--ca_dir', parser.add_argument('--ca_dir',
......
body {
min-height: 2000px;
padding-top: 70px;
}
\ No newline at end of file
<!doctype html>
<title>Certificate Authority web</title>
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}">
<div class=page>
<h1>Certificate Authority Public</h1>
<ul>
{% for filename in filename_list -%}
<li><a href="/get/{{ filename }}">{{ filename }}</a></li>
{% endfor -%}
</ul>
</div>
\ No newline at end of file
# -*- coding: utf-8 -*-
import os
import sys
import traceback
from OpenSSL import crypto
import uuid
from flask import (Flask, request, redirect, url_for, render_template,
jsonify, session, abort, send_file)
app = Flask(__name__)
@app.errorhandler(403)
def error403(res):
return "403: Forbidden. Your are not allowed to access %s" % res, 403
return response
@app.errorhandler(404)
def error404(msg=""):
return "404: Resource not found.\n%s\n" % msg, 404
@app.errorhandler(501)
def error501(msg=""):
return "501: Internal Error. %s\n" % msg, 501
@app.errorhandler(412)
def error412(msg):
return "412: Precondition Failed: %s.\n" % msg, 412
@app.errorhandler(400)
def error400():
return "400: Bad Request", 400
def writefile(path, content, mode=0640):
with open(path, 'w') as fd:
fd.write(content)
os.chmod(path, mode)
@app.route('/')
def index():
# page to list certificates, also connection link
cert_list = [
os.path.basename(app.config.ca.certificate),
os.path.basename(app.config.ca.ca_crl)
]
cert_list.extend([x for x in os.listdir(app.config.cert_dir)])
return render_template("index.html", filename_list=cert_list)
@app.route('/get/<string:name>', methods=['GET'])
def getfile(name):
ca_name = os.path.basename(app.config.ca.certificate)
crl_name = os.path.basename(app.config.ca.ca_crl)
if name == ca_name:
return send_file(app.config.ca.certificate,
attachment_filename=ca_name,
as_attachment=True)
elif name == crl_name:
return send_file(app.config.ca.ca_crl,
attachment_filename=crl_name,
as_attachment=True)
else:
cert_file = os.path.join(app.config.cert_dir, name)
if os.path.exists(cert_file) and os.path.isfile(cert_file):
filename = os.path.basename(cert_file)
return send_file(cert_file,
attachment_filename=filename,
as_attachment=True)
return abort(404, name)
@app.route('/request', methods=['POST'])
def do_request():
csr_content = request.form.get('csr', '').encode('utf-8')
if not csr_content:
return abort(400)
try:
req = app.config.ca.readCertificateRequest(csr_content)
# XXX - Maybe more check if the csr is valid with CA
except crypto.Error, e:
return abort(412, str(e))
cert_id = str(uuid.uuid1())
csr_keyfile = '%s+%s.csr.pem' % (cert_id, str(uuid.uuid4()))
request_file = os.path.join(app.config.req_dir, csr_keyfile)
if os.path.exists(request_file):
# The request already exist
raise Exception("Certificate Signature Request file should be unique")
try:
writefile(request_file, csr_content)
except OSError, e:
raise
return cert_id
@app.route('/signcert', methods=['POST'])
def do_signcert():
"""
This method should be called by a list of host allowed, can be used to sign with command line
For security, it's good to only allow local ip (127.0.0.1)
"""
key = request.form.get('key', '').encode('utf-8')
if not key:
return abort(400)
remote_client = request.remote_addr
x_forwarded_for_list = request.headers.getlist("X-Forwarded-For")
if remote_client not in app.config.trusted_host_list or \
(x_forwarded_for_list and x_forwarded_for_list[0] not in app.config.trusted_host_list):
return abort(403) # Forbidden
req_file = os.path.join(app.config.req_dir, '%s.csr.pem' % key)
if os.path.exists(req_file):
cert_id = key.split('+')[0]
output = os.path.join(app.config.cert_dir, '%s.cert.pem' % cert_id)
try:
if os.path.exists(output):
# This should not happend normally
return abort(412, "The certificate already exists.")
app.config.ca.signCertificateRequest(req_file, output)
os.rename(req_file, os.path.join(app.config.csr_dir, '%s.csr.pem' % cert_id))
except Exception, e:
if os.path.exists(output):
try:
os.unlink(output)
except:
pass
traceback.print_exc()
return abort(501, str(e))
else:
return abort(403)
return "Certificate is signed"
@app.route('/renewcert/<string:serial>', methods=['POST'])
def renewcert(serial):
"""
this method is used to renew expired certificate.
"""
return abort(200, "Done. The method is not implemented yet!")
import os
import datetime
from flask_user import UserMixin
from slapos.certificate_authority.web.start_web import db
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, validators
CERT_STATUS_VALIDATED = 'validated'
CERT_STATUS_REVOKED = 'invalidated'
CERT_STATUS_REJECTED = 'rejected'
CERT_STATUS_PENDING = 'pending'
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
# User authentication information
username = db.Column(db.String(50), nullable=False, unique=True)
password = db.Column(db.String(255), nullable=False, server_default='')
# User email information
email = db.Column(db.String(255), nullable=False, unique=True)
confirmed_at = db.Column(db.DateTime())
# User information
active = db.Column('is_active', db.Boolean(), nullable=False, server_default='0')
first_name = db.Column(db.String(100), nullable=False, server_default='')
last_name = db.Column(db.String(100), nullable=False, server_default='')
class Certificate(db.Model):
"""
This table is used to store some informations about certificate
"""
__tablename__ = 'certificate'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True)
serial = db.Column(db.String(50), unique=True)
filename = db.Column(db.String(80), unique=True)
common_name = db.Column(db.String(50), unique=False)
expire_after = db.Column(db.DateTime)
start_before = db.Column(db.DateTime)
# status = validated or revoked
status = db.Column(db.String(20), unique=False, server_default=CERT_STATUS_VALIDATED)
comment = db.Text(db.String())
# When renew, this can say what was the previous certificate
old_serial = db.Column(db.String(50), unique=False, server_default='')
def __repr__(self):
return '<CertificateMap %r>' % (self.serial)
class Revoke(db.Model):
"""
This table contains information about certificate revocation
"""
__tablename__ = 'revoke'
id = db.Column(db.Integer, primary_key=True)
comment = db.Column(db.Text())
serial = db.Column(db.String(50), unique=True)
revoke_date = db.Column(db.DateTime)
# link to revoke request if was requested by users
revoke_request_id = db.Column(db.Integer, server_default='')
def __repr__(self):
return '<CertificateMap %r>' % (self.serial)
class RevokeRequest(db.Model):
"""
This table store certificate revocation request from users
"""
__tablename__ = 'revoke_request'
id = db.Column(db.Integer, primary_key=True)
serial = db.Column(db.String(50), unique=False)
filename = db.Column(db.String(80), unique=False)
common_name = db.Column(db.String(50), unique=False)
reason = db.Column(db.String(200), unique=False)
request_date = db.Column(db.DateTime)
status = db.Column(db.String(20), unique=False, server_default=CERT_STATUS_PENDING)
def __repr__(self):
return '<CertificateMap %r>' % (self.serial)
class DisabledStringField(StringField):
def __call__(self, *args, **kwargs):
kwargs.setdefault('disabled', True)
return super(ReadonlyStringField, self).__call__(*args, **kwargs)
# Define the User profile form
class UserProfileForm(FlaskForm):
username = StringField('Username')
first_name = StringField('First name', validators=[
validators.DataRequired('First name is required')])
last_name = StringField('Last name', validators=[
validators.DataRequired('Last name is required')])
email = StringField('Email', validators=[
validators.DataRequired('Email is required')])
submit = SubmitField('Save')
import os
# Application settings
APP_NAME = "Certificate Authority web app"
# DO NOT use "DEBUG = True" in production environments
DEBUG = True
# DO NOT use Unsecure Secrets in production environments
# Generate a safe one with:
# python -c "import os; print repr(os.urandom(24));"
SECRET_KEY = 'This is an UNSECURE Secret. CHANGE THIS for production environments.'
# SQLAlchemy settings
SQLALCHEMY_DATABASE_URI = 'sqlite:///ca.db'
SQLALCHEMY_TRACK_MODIFICATIONS = False
CSRF_ENABLED = True
# Flask-Mail settings
# For smtp.gmail.com to work, you MUST set "Allow less secure apps" to ON in Google Accounts.
# Change it in https://myaccount.google.com/security#connectedapps (near the bottom).
MAIL_SERVER = 'smtp.gmail.com'
MAIL_PORT = 587
MAIL_USE_SSL = False
MAIL_USE_TLS = True
MAIL_USERNAME = 'yourname@gmail.com'
MAIL_PASSWORD = 'password'
MAIL_DEFAULT_SENDER = '"Your Name" <yourname@gmail.com>'
# Used by email templates
USER_APP_NAME = "Certificate Authority"
# Internal application
USER_AFTER_LOGIN_ENDPOINT = ''
USER_AFTER_LOGOUT_ENDPOINT = ''
USER_ENABLE_USERNAME = True
USER_ENABLE_EMAIL = False
USER_ENABLE_REGISTRATION = False
USER_ENABLE_CHANGE_USERNAME = False
\ No newline at end of file
from flask_sqlalchemy import SQLAlchemy
from flask_user import UserManager, SQLAlchemyAdapter
from flask import Flask
from flask_mail import Mail
import os
app = Flask(__name__)
db = SQLAlchemy(app)
def init_app():
mail = Mail(app)
#db.init_app(app)
db.create_all()
# Setup Flask-User
from slapos.certificate_authority.web.models import User
db_adapter = SQLAlchemyAdapter(db, User) # Register the User model
user_manager = UserManager(db_adapter, app) # Initialize Flask-User
\ No newline at end of file
.ui-table th, .ui-table td {
line-height: 1.5em;
text-align: left;
padding: .4em .5em;
vertical-align: middle;
}
.ui-overlay-a, .ui-page-theme-a, .ui-page-theme-a .ui-panel-wrapper {
background-color: #fff !important;
}
table .ui-table th, table .ui-table td {
vertical-align: middle;
}
.noshadow buton, .noshadow a, .noshadow input, .noshadow select {
-webkit-box-shadow: none !important;
-moz-box-shadow: none !important;
box-shadow: none !important;
}
.ui-error, html .ui-content .ui-error a, .ui-content a.ui-error {
color: red;
font-weight: bold;
}
html body {
overflow-x: hidden;
background: #fbfbfb;
}
.form-signin {
max-width: 330px;
padding: 15px;
margin: 0 auto;
}
.form-signin .form-signin-heading,
.form-signin .checkbox {
margin-bottom: 10px;
}
.form-signin .checkbox {
font-weight: normal;
}
.form-signin .form-control {
position: relative;
height: auto;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
padding: 10px;
font-size: 16px;
}
.form-signin .form-control:focus {
z-index: 2;
}
.form-signin input[type="email"] {
margin-bottom: -1px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
/* Toggle Styles */
#wrapper {
padding-left: 0;
-webkit-transition: all 0.6s ease;
-moz-transition: all 0.6s ease;
-o-transition: all 0.6s ease;
transition: all 0.6s ease;
}
#wrapper.toggled {
padding-left: 200px;
}
#sidebar-wrapper {
z-index: 1000;
position: fixed;
left: 250px;
width: 0;
height: 100%;
margin-left: -250px;
overflow-y: auto;
background-color:#312A25 !Important;
-webkit-transition: all 0.2s ease;
-moz-transition: all 0.2s ease;
-o-transition: all 0.2s ease;
transition: all 0.2s ease;
}
#wrapper.toggled #sidebar-wrapper {
width: 0;
}
#page-content-wrapper {
width: 100%;
position: absolute;
padding: 10px;
}
#wrapper.toggled #page-content-wrapper {
position: absolute;
margin-left:-250px;
}
/* Sidebar Styles */
.nav-side-menu {
overflow: auto;
font-family: verdana;
font-size: 12px;
font-weight: 200;
background-color: #2a2f35; /* #313130 */
position: fixed;
top: 0px;
width: 300px;
height: 100%;
color: #e1ffff;
}
.nav-side-menu .brand {
background-color: #404040;
line-height: 50px;
display: block;
text-align: center;
font-size: 14px;
}
.nav-side-menu .toggle-btn {
display: none;
}
.nav-side-menu ul,
.nav-side-menu li {
list-style: none;
padding: 0px;
margin: 0px;
line-height: 35px;
cursor: pointer;
/*
.collapsed{
.arrow:before{
font-family: FontAwesome;
content: "\f053";
display: inline-block;
padding-left:10px;
padding-right: 10px;
vertical-align: middle;
float:right;
}
}
*/
}
.nav-side-menu ul :not(collapsed) .arrow:before,
.nav-side-menu li :not(collapsed) .arrow:before {
font-family: FontAwesome;
content: "\f078";
display: inline-block;
padding-left: 10px;
padding-right: 10px;
vertical-align: middle;
float: right;
}
.nav-side-menu ul .active,
.nav-side-menu li .active {
border-left: 3px solid #d19b3d;
background-color: #4f5b69;
}
.nav-side-menu ul .sub-menu li.active,
.nav-side-menu li .sub-menu li.active {
color: #d19b3d;
}
.nav-side-menu ul .sub-menu li.active a,
.nav-side-menu li .sub-menu li.active a {
color: #d19b3d;
}
.nav-side-menu ul .sub-menu li,
.nav-side-menu li .sub-menu li {
background-color: #181c20;
border: none;
line-height: 28px;
border-bottom: 1px solid #23282e;
margin-left: 0px;
}
.nav-side-menu ul .sub-menu li:hover,
.nav-side-menu li .sub-menu li:hover {
background-color: #020203;
}
.nav-side-menu ul .sub-menu li:before,
.nav-side-menu li .sub-menu li:before {
font-family: FontAwesome;
content: "\f105";
display: inline-block;
padding-left: 10px;
padding-right: 10px;
vertical-align: middle;
}
.nav-side-menu li {
padding-left: 0px;
border-left: 3px solid #2e353d;
border-bottom: 1px solid #23282e;
}
.nav-side-menu li a {
text-decoration: none;
color: #e1ffff;
display: block;
}
.nav-side-menu li a i {
padding-left: 10px;
width: 20px;
padding-right: 25px;
font-size: 18px;
}
.nav-side-menu li:hover {
border-left: 3px solid #d19b3d;
background-color: #4f5b69;
-webkit-transition: all 1s ease;
-moz-transition: all 1s ease;
-o-transition: all 1s ease;
-ms-transition: all 1s ease;
transition: all 1s ease;
}
@media (max-width: 767px) {
.nav-side-menu {
position: relative;
width: 100%;
margin-bottom: 10px;
}
.nav-side-menu .toggle-btn {
display: block;
cursor: pointer;
position: absolute;
right: 10px;
top: 10px;
z-index: 10 !important;
padding: 3px;
background-color: #ffffff;
color: #000;
width: 40px;
text-align: center;
}
.brand {
text-align: left !important;
font-size: 22px;
padding-left: 20px;
line-height: 50px !important;
}
}
@media (min-width: 767px) {
.nav-side-menu .menu-list .menu-content {
display: block;
}
#main {
width:calc(100% - 300px);
float: right;
}
}
body {
margin: 0px;
padding: 0px;
}
pre {
max-height: 600px;
}
.col-centered {
float: none;
margin: 0 auto;
}
.clickable{
cursor: pointer;
}
.table .panel-heading div {
margin-top: -18px;
font-size: 15px;
}
.table .panel-heading div span{
margin-left:5px;
}
.table .panel-body{
display: none;
}
.container .table>tbody>tr>td, .table>tbody>tr>th, .table>tfoot>tr>td, .table>tfoot>tr>th, .table>thead>tr>td, .table>thead>tr>th {
vertical-align: middle;
}
.margin-top-40 {
margin-top:40px;
}
.margin-lr-20 {
margin: 0 20px;
}
.flashes-messages div:first-child {
margin-top: 30px;
}
/* Dashboard boxes */
.dash-panel {
text-align: center;
padding: 1px 0;
}
html body a:hover > .dash-panel h4 {
text-decoration: none;
}
.dash-panel:hover {
background-color: #e6e6e6;
border-color: #adadad;
cursor: pointer;
}
.dash {
position: relative;
text-align: center;
width: 120px;
height: 55px;
margin: 10px auto 10px auto;
}
#dash-blue .number {
color: #30a5ff;
}
#dash-orange .number {
color: #ffb53e;
}
#dash-teal .number {
color: #1ebfae;
}
#dash-red .number {
color: #ef4040;
}
#dash-darkred .number {
color: #bd0849;
}
.dash .number {
display: block;
position: absolute;
font-size: 46px;
width: 120px;
}
.alert-error {
color: #a94442;
background-color: #f2dede;
border-color: #ebccd1;
}
$( document ).ready(function() {
$("#menu-toggle").click(function(e) {
e.preventDefault();
$("#wrapper").toggleClass("toggled");
});
});
(function(){
'use strict';
var $ = jQuery;
$.fn.extend({
filterTable: function(){
return this.each(function(){
$(this).on('keyup', function(e){
$('.filterTable_no_results').remove();
var $this = $(this),
search = $this.val().toLowerCase(),
target = $this.attr('data-filters'),
$target = $(target),
$rows = $target.find('tbody tr');
if(search === '') {
$rows.show();
} else {
$rows.each(function(){
var $this = $(this);
$this.text().toLowerCase().indexOf(search) === -1 ? $this.hide() : $this.show();
});
if($target.find('tbody tr:visible').size() === 0) {
var col_count = $target.find('tr').first().find('td').size();
var no_results = $('<tr class="filterTable_no_results"><td colspan="'+col_count+'">No results found</td></tr>');
$target.find('tbody').append(no_results);
}
}
});
});
}
});
$('[data-action="filter"]').filterTable();
})(jQuery);
$(function(){
// attach table filter plugin to inputs
$('[data-action="filter"]').filterTable();
$('.container').on('click', '.panel-heading span.filter', function(e){
var $this = $(this),
$panel = $this.parents('.panel');
$panel.find('.panel-body').slideToggle();
if($this.css('display') != 'none') {
$panel.find('.panel-body input').focus();
}
});
$('[data-toggle="tooltip"]').tooltip();
});
\ No newline at end of file
{% extends "layout.html" %}
{% block content %}
<form class="form-signin" method="POST" action="/setpassword">
<h2 class="form-signin-heading" style="margin-bottom: 30px">Set admin password</h2>
<label for="pw" class="sr-only">Password</label>
<input type="inputPassword" name="password" id="pw" class="form-control" placeholder="password" required autofocus>
<label for="pw2" class="sr-only">Confirm Password:</label>
<input type="inputPassword" name="password2" id="pw2" class="form-control" placeholder="Confirm password" required autofocus>
<br/>
<button class="btn btn-lg btn-primary btn-block" type="submit">configure</button>
</form>
{% endblock %}
{% extends "layout.html" %}
{% block pre_content %}
<div class="row">
<div class="col-sm-7 col-md-6 col-lg-5 col-centered">
{% endblock %}
{% block post_content %}
</div>
</div>
{% endblock %}
\ No newline at end of file
{% extends 'flask_user/flask_user_base.html' %}
\ No newline at end of file
{% extends 'flask_user/flask_user_base.html' %}
\ No newline at end of file
<!-- Placed at the end of the document so the pages load faster -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<script type=application/javascript src="{{ url_for('static', filename='scripts/index.js') }}"></script>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Certificate Authority web</title>
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='css/styles.css') }}">
<link href="//netdna.bootstrapcdn.com/font-awesome/4.2.0/css/font-awesome.min.css" rel="stylesheet" type="text/css" />
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
{% extends "layout.html" %}
{% block content %}
<div class="page-header">
<h1>Public Certificates <small>Certificate authority web</small></h1>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-success margin-top-40 table">
<div class="panel-heading">
<h3 class="panel-title">List of signed Certificates</h3>
<div class="pull-right">
<span class="clickable filter" data-toggle="tooltip" title="Toggle table filter" data-container="body">
<i class="glyphicon glyphicon-filter"></i>
</span>
</div>
</div>
<div class="panel-body">
<input type="text" class="form-control" id="cacert-table-filter" data-action="filter" data-filters="#cacert-table" placeholder="Filter Columns" />
</div>
<table class="table table-hover" id="cacert-table">
<thead>
<tr>
<th>#</th>
<th>Name</th>
<th>Common Name</th>
<th>Expiration Date</th>
<th></th>
</tr>
</thead>
<tbody>
{% for cert in data_list -%}
<tr>
<td>{{ cert['index'] }}</td>
{% if cert['filename'] == 'cacert.pem' -%}
<td><a href="/viewcert?name={{ cert['filename'] }}&type=cacert">{{ cert['name'] }}</a></td>
{% elif cert['filename'] == 'cacrl.pem' -%}
<td><a href="/viewcert?name={{ cert['filename'] }}&type=crl">{{ cert['name'] }}</a></td>
{% else -%}
<td><a href="/viewcert?name={{ cert['filename'] }}&type=X509">{{ cert['name'] }}</a></td>
{% endif -%}
<td>{{ cert['cn'] }}</td>
<td>{{ cert['expiration_date'] }}</td>
<td><a class="btn btn-default" href="/get/{{ cert['name'] }}" role="button" title="Download file"><i class="fa fa-download" aria-hidden="true"></i></a></td>
</tr>
{% endfor -%}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
{% include "head.html" %}
</head>
<body>
{% block body %}
<div class="container-fluid">
<div class="row">
<div class="nav-side-menu">
<div class="brand">Certificate Authority</div>
<i class="fa fa-bars fa-2x toggle-btn" data-toggle="collapse" data-target="#menu-content"></i>
<div class="menu-list">
<ul id="menu-content" class="menu-content collapse out">
<li>
<a href="/">
<i class="fa fa-home" aria-hidden="true"></i> Public Home
</a>
</li>
{% if not session.user_id %}
<li>
<a href="/manage">
<i class="fa fa-cog" aria-hidden="true"></i> Manage Certificates
</a>
</li>
{% else -%}
<li><a href="/manage"><i class="fa fa-tachometer" aria-hidden="true"></i> Admin Dashboard</a></li>
<li><a href="/signed_certs"><i class="fa fa-check-square" aria-hidden="true"></i> Signed Certificates</a></li>
<li><a href="/revoked_certs"><i class="fa fa-minus-square" aria-hidden="true"></i> Revoked Certificates</a></li>
<li><a href="/profile"><i class="fa fa-user" aria-hidden="true"></i> Parameters</a></li>
<li><a href="/view_logs"><i class="fa fa-book" aria-hidden="true"></i> Certificate Authority Logs</a></li>
<li><a href="/logout"><i class="fa fa-sign-out" aria-hidden="true"></i> Logout</a></li>
{% endif -%}
</ul>
</div>
</div>
<div class="container" id="main">
<div class="flashes-messages">
{% with messages = get_flashed_messages(with_categories=true) %}
<!-- Categories: success (green), info (blue), warning (yellow), danger (red) -->
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
{{ message|safe }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
{% block pre_content %}{% endblock %}
{% block content %}{% endblock %}
{% block post_content %}{% endblock %}
</div>
</div>
</div>
{% endblock %}
{% include "footer.html" %}
</body>
</html>
\ No newline at end of file
{% macro render_field(field, label=None, label_visible=true, right_url=None, right_label=None, readonly=false) -%}
<div class="form-group {% if field.errors %}has-error{% endif %} {{ kwargs.pop('class_', '') }}">
{% if field.type != 'HiddenField' and label_visible %}
{% if not label %}{% set label=field.label.text %}{% endif %}
<label for="{{ field.id }}" class="control-label">{{ label|safe }}</label>
{% endif %}
{{ field(class_='form-control', readonly=readonly, **kwargs) }}
{% if field.errors %}
{% for e in field.errors %}
<p class="help-block">{{ e }}</p>
{% endfor %}
{% endif %}
</div>
{%- endmacro %}
{% macro render_checkbox_field(field, label=None) -%}
{% if not label %}{% set label=field.label.text %}{% endif %}
<div class="checkbox">
<label>
{{ field(type='checkbox', **kwargs) }} {{ label }}
</label>
</div>
{%- endmacro %}
{% macro render_radio_field(field) -%}
{% for value, label, checked in field.iter_choices() %}
<div class="radio">
<label>
<input type="radio" name="{{ field.id }}" id="{{ field.id }}" value="{{ value }}"{% if checked %} checked{% endif %}>
{{ label }}
</label>
</div>
{% endfor %}
{%- endmacro %}
{% macro render_submit_field(field, label=None, tabindex=None) -%}
{% if not label %}{% set label=field.label.text %}{% endif %}
{#<button type="submit" class="form-control btn btn-lg btn-primary btn-block">{{label}}</button>#}
<input type="submit" class="btn btn-lg btn-primary btn-block" value="{{label}}"
{% if tabindex %}tabindex="{{ tabindex }}"{% endif %}
>
{%- endmacro %}
\ No newline at end of file
{% extends "layout.html" %}
{% block content %}
<div class="page-header">
<h1>Administration <small>Certificate authority web</small></h1>
</div>
<div class="row">
<div class="col-xs-6 col-md-3">
<a href="/manage">
<div class="panel panel-default">
<div class="panel-body dash-panel">
<h4><strong>Signature Requests</strong></h4>
<div class="dash" id="dash-orange">
<span class="number">{{ counter['csr'] }}</span></div>
</div>
</div>
</a>
</div>
<div class="col-xs-6 col-md-3">
<a href="/signed_certs">
<div class="panel panel-default">
<div class="panel-body dash-panel">
<h4><strong>Signed Certificates</strong></h4>
<div class="dash" id="dash-teal">
<span class="number">{{ counter['cert'] }}</span>
</div>
</div>
</div>
</a>
</div>
<div class="col-xs-6 col-md-3">
<a href="/revocation_requests">
<div class="panel panel-default">
<div class="panel-body dash-panel">
<h4><strong>Revoke Requests</strong></h4>
<div class="dash" id="dash-red">
<span class="number">{{ counter['crr'] }}</span></div>
</div>
</div>
</a>
</div>
<div class="col-xs-6 col-md-3">
<a href="/revoked_certs">
<div class="panel panel-default">
<div class="panel-body dash-panel">
<h4><strong>Revoked</strong></h4>
<div class="dash" id="dash-darkred">
<span class="number">{{ counter['crl'] }}</span>
</div>
</div>
</div>
</a>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-primary margin-top-40 table">
<div class="panel-heading">
<h3 class="panel-title">Certificates Signature request list</h3>
<div class="pull-right">
<span class="clickable filter" data-toggle="tooltip" title="Toggle table filter" data-container="body">
<i class="glyphicon glyphicon-filter"></i>
</span>
</div>
</div>
<div class="panel-body">
<input type="text" class="form-control" id="cacert-table-filter" data-action="filter" data-filters="#cacert-table" placeholder="Filter Columns" />
</div>
<table class="table table-hover" id="cacert-table">
<thead>
<tr>
<th class="hidden-xs">#</th>
<th class="hidden-xs">CSR Key</th>
<th>Common Name</th>
<th class="hidden-xs">Request Date</th>
<th></th>
</tr>
</thead>
<tbody>
{% for csr in data_list -%}
<tr>
<td class="hidden-xs">{{ csr['index'] }}</td>
<td class="hidden-xs">{{ csr['key'] }}</td>
<td>{{ csr['cn'] }}</td>
<td class="hidden-xs">{{ csr['request_date'] }}</td>
<td>
<div class="btn-group">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Action <span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li><a href="/signcert_web?name={{ csr['filename_quote'] }}"><i class="fa fa-check-square" aria-hidden="true"></i> Sign CSR</a></li>
<li><a href="/viewcert?name={{ csr['filename_quote'] }}&type=req"><i class="fa fa-eye" aria-hidden="true"></i> View details</a></li>
<li role="separator" class="divider"></li>
<li><a href="/deletecsr?name={{ csr['filename_quote'] }}"><i class="fa fa-times" aria-hidden="true"></i> Delete CSR</a></li>
</ul>
</div>
</td>
</tr>
{% endfor -%}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}
\ No newline at end of file
{% extends "layout.html" %}
{% block content %}
<div class="page-header">
<h1>Revocation Requests <small>Certificate revocation send by users</small></h1>
</div>
<div class="row">
<div class="col-md-12"><h4>Click on <strong>Revoke</strong> button to validate a revocation request. Revoked certificate will be added to cacrl.pem file.</h4></div>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-danger margin-top-40 table">
<div class="panel-heading">
<h3 class="panel-title">All Revocation Requests</h3>
<div class="pull-right">
<span class="clickable filter" data-toggle="tooltip" title="Toggle table filter" data-container="body">
<i class="glyphicon glyphicon-filter"></i>
</span>
</div>
</div>
<div class="panel-body">
<input type="text" class="form-control" id="cacert-table-filter" data-action="filter" data-filters="#cacert-table" placeholder="Filter Columns" />
</div>
<table class="table table-hover" id="cacert-table">
<thead>
<tr>
<th>#</th>
<th>serial</th>
<th>Filename</th>
<th>State</th>
<th>Common Name</th>
<th>Request Date</th>
<th>Reason</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
{% for request in data_list -%}
<tr>
<td>{{ request['index'] }}</td>
<td><a href="/viewcert?name={{ request['filename'] }}&type=X509">{{ request['serial'] }}</a></td>
<td><a href="/viewcert?name={{ request['filename'] }}&type=X509">{{ request['filename'] }}</a></td>
<td><span style="color: #1ebfae; font-weight: bold">{{ request['status'] }}</span></td>
<td>{{ request['cn'] }}</td>
<td>{{ request['request_date'] }}</td>
<td>{{ request['reason'] }}</td>
{% if not request['frozen'] -%}
<td><a class="btn btn-danger" href="/revoke_cert?request_id={{ request['id'] }}&serial={{ request['serial'] }}" role="button" title="Revoke certificate">
<i class="fa fa-arrow-circle-right" aria-hidden="true"></i> Revoke</a>
</td>
<td><a class="btn btn-default" href="/reject_revocation_request?request_id={{ request['id'] }}" role="button" title="Reject request">
<i class="fa fa-times" aria-hidden="true"></i> Reject</a>
</td>
{% else -%}
<td>--</td>
<td>--</td>
{% endif -%}
</tr>
{% endfor -%}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}
\ No newline at end of file
{% extends "layout.html" %}
{% block content %}
<div class="page-header">
<h1>Invalidated Certificates <small>revoked by admin</small></h1>
</div>
<div class="row">
<div class="margin-lr-20">
<a class="btn btn-default" href="/generate_crl" role="button" title="Regenerate Certificate Revocation List file">
<i class="fa fa-refresh" aria-hidden="true"></i> Re-Generate CRL file</a>
<a class="btn btn-default" href="/revocation_requests" role="button" title="View Revocation Request list">
<i class="fa fa-external-link-square" aria-hidden="true"></i> Revocation Requests</a>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-danger margin-top-40 table">
<div class="panel-heading">
<h3 class="panel-title">All Signed Certificates</h3>
<div class="pull-right">
<span class="clickable filter" data-toggle="tooltip" title="Toggle table filter" data-container="body">
<i class="glyphicon glyphicon-filter"></i>
</span>
</div>
</div>
<div class="panel-body">
<input type="text" class="form-control" id="cacert-table-filter" data-action="filter" data-filters="#cacert-table" placeholder="Filter Columns" />
</div>
<table class="table table-hover" id="cacert-table">
<thead>
<tr>
<th>#</th>
<th>serial</th>
<th>Name</th>
<th>Filename</th>
<th>Common Name</th>
<th>Start Date</th>
<th>Expiration Date</th>
</tr>
</thead>
<tbody>
{% for cert in data_list -%}
<tr>
<td>{{ cert['index'] }}</td>
<td><a href="/viewcert?name={{ cert['filename'] }}&type=X509">{{ cert['serial'] }}</a></td>
<td><a href="/viewcert?name={{ cert['filename'] }}&type=X509">{{ cert['name'] }}</a></td>
<td><a href="/viewcert?name={{ cert['filename'] }}&type=X509">{{ cert['filename'] }}</a></td>
<td>{{ cert['cn'] }}</td>
<td>{{ cert['start_date'] }}</td>
<td>{{ cert['expiration_date'] }}</td>
</tr>
{% endfor -%}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}
\ No newline at end of file
{% extends "layout.html" %}
{% block content %}
<div class="page-header">
<h1>Signed Certificates <small>requested by end users</small></h1>
</div>
<div class="row">
<div class="col-md-12"><h4>Click on <strong>Revoke</strong> button to revoke a certificate. Revoked certificate will be added to cacrl.pem file.</h4></div>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-primary margin-top-40 table">
<div class="panel-heading">
<h3 class="panel-title">All Signed Certificates</h3>
<div class="pull-right">
<span class="clickable filter" data-toggle="tooltip" title="Toggle table filter" data-container="body">
<i class="glyphicon glyphicon-filter"></i>
</span>
</div>
</div>
<div class="panel-body">
<input type="text" class="form-control" id="cacert-table-filter" data-action="filter" data-filters="#cacert-table" placeholder="Filter Columns" />
</div>
<table class="table table-hover" id="cacert-table">
<thead>
<tr>
<th>#</th>
<th>serial</th>
<th>Filename</th>
<th>Common Name</th>
<th>Start Date</th>
<th>Expiration Date</th>
<th></th>
</tr>
</thead>
<tbody>
{% for cert in data_list -%}
<tr>
<td>{{ cert['index'] }}</td>
<td><a href="/viewcert?name={{ cert['filename'] }}&type=X509">{{ cert['serial'] }}</a></td>
<td><a href="/viewcert?name={{ cert['filename'] }}&type=X509">{{ cert['filename'] }}</a></td>
<td>{{ cert['cn'] }}</td>
<td>{{ cert['start_date'] }}</td>
<td>{{ cert['expiration_date'] }}</td>
<td><a class="btn btn-danger" href="/revoke_cert?serial={{ cert['serial'] }}" role="button" title="Revoke certificate">
<i class="fa fa-times" aria-hidden="true"></i> Revoke</a>
</td>
</tr>
{% endfor -%}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}
\ No newline at end of file
{% extends "layout.html" %}
{% block content %}
<div class="page-header">
<h1>User account information</h1>
</div>
<div style="padding: 15px">
<p>
<a class="btn btn-default" href="{{ url_for('user.change_password') }}" role="button" title="Download file">
<i class="fa fa-pencil-square-o" aria-hidden="true"></i> Change password</a>
</p>
{% from "macros.html" import render_field, render_submit_field %}
<form action="" method="POST" class="form" role="form">
<div class="row">
<div class="col-sm-6 col-md-5 col-lg-4">
{{ form.hidden_tag() }}
{{ render_field(form.username, tabindex=230, readonly=true) }}
{{ render_field(form.first_name, tabindex=240) }}
{{ render_field(form.last_name, tabindex=250) }}
{{ render_field(form.email, tabindex=260) }}
{{ render_submit_field(form.submit, tabindex=280) }}
</div>
</div>
</form>
</div>
{% endblock %}
\ No newline at end of file
{% extends "layout.html" %}
{% block content %}
<div class="page-header">
<h1>View {{ cert_type }} Details <small></small></h1>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{{ name |safe }}</h3>
</div>
<div class="panel-body">
<pre>{{ content }}</pre>
</div>
</div>
</div>
</div>
{% endblock %}
\ No newline at end of file
{% extends "layout.html" %}
{% block content %}
<div class="page-header">
<h1>Certificate Authority <small>view logs content</small></h1>
</div>
<div class="row">
<div class="col-md-12">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{{ filename }}</h3>
</div>
<div class="panel-body">
<p>
<a class="btn btn-default" href="/view_logs?full=true" role="button" title="Download file">
<i class="fa fa-file-text" aria-hidden="true"></i> Full log</a>
<a class="btn btn-default" href="/view_logs" role="button" title="Download file">
<i class="fa fa-file-text" aria-hidden="true"></i> Tail log</a>
</p>
<pre style="max-height: 600px;">{{ content }}</pre>
</div>
</div>
</div>
</div>
{% endblock %}
\ No newline at end of file
# -*- coding: utf-8 -*-
import os
import time
import urllib
from datetime import datetime
from slapos.certificate_authority.web.start_web import app, db
from slapos.certificate_authority.web.models import (User, Certificate,
RevokeRequest, Revoke, CERT_STATUS_VALIDATED, CERT_STATUS_REVOKED,
CERT_STATUS_PENDING, CERT_STATUS_REJECTED)
from slapos.certificate_authority.certificate_authority import CertificateBase
def find_or_create_user(first_name, last_name, email, username, password):
""" Find existing user or create new user """
user = User.query.filter(User.username == username).first()
if not user:
user = User(email=email,
first_name=first_name,
last_name=last_name,
username=username,
password=app.user_manager.hash_password(password),
active=True,
confirmed_at=datetime.utcnow()
)
db.session.add(user)
db.session.commit()
return user
def find_user(username):
return User.query.filter(User.username == username).first()
def get_string_num(number):
if number < 10:
return '0%s' % number
return str(number)
class CertificateTools(object):
def signCertificate(self, cert_id, req_file):
"""
Sign a certificate, cert_id is the name used by the user to download the cert
"""
csr_dest = os.path.join(app.config.csr_dir, '%s.csr.pem' % cert_id)
try:
# Avoid signing two certificate at the same time (for unique serial)
app.config.ca._lock()
next_serial = app.config.ca.getNextSerial()
output = os.path.join(app.config.cert_dir, '%s.cert.pem' % next_serial)
# This should not happend normally
assert not os.path.exists(output), output
# Test that we can write on csr file
os.close(os.open(req_file, os.O_RDWR | os.O_APPEND))
app.config.ca.signCertificateRequest(req_file, output)
cert = app.config.ca.freadX509(output)
cert_db = Certificate(
name='%s.cert.pem' % cert_id,
serial=next_serial,
filename='%s.cert.pem' % next_serial,
common_name=cert.get_subject().CN,
expire_after=datetime.strptime(cert.get_notAfter(), '%Y%m%d%H%M%SZ'),
start_before=datetime.strptime(cert.get_notBefore(), '%Y%m%d%H%M%SZ')
)
db.session.add(cert_db)
db.session.commit()
os.rename(req_file, csr_dest)
except Exception, e:
if os.path.exists(output):
try:
# Revoke Certificate because problem occurred
# No need to update CRL because the certificate was not used.
app.config.ca.revokeCertificate(output)
except:
pass
try:
# Revoke certificate from database if exists
db_cert = Certificate.query.filter(Certificate.serial == next_serial).first()
if db_cert:
db_cert.status = CERT_STATUS_REVOKED
db_cert.comment = "Auto revoked due to error: %s" % str(e)
db.session.commit()
except:
pass
raise
finally:
app.config.ca._unlock()
def addRevokeRequest(self, serial, hash_name, message):
cert_path = os.path.join(app.config.cert_dir, '%s.cert.pem' % serial)
if not os.path.exists(cert_path):
# This check is fast but 'serial'.cert.pem should the the cert filename in db
return False
cert = Certificate.query.filter(
Certificate.status == CERT_STATUS_VALIDATED
).filter(Certificate.serial == serial).first()
if not cert or cert.name != '%s.cert.pem' % hash_name:
# This certificate not found or not match
return False
# Create Request
req = RevokeRequest(
serial=serial,
filename=cert.filename,
common_name=cert.common_name,
request_date=datetime.utcnow(),
reason=message
)
db.session.add(req)
db.session.commit()
return True
def cancelRevocationRequest(self, req_id):
req = RevokeRequest.query.filter(
RevokeRequest.id == req_id
).first()
if req and req.status != CERT_STATUS_REJECTED:
req.status = CERT_STATUS_REJECTED
db.session.commit()
return True
return False
def revokeCertificate(self, serial, request_id=None):
request_entry = None
if request_id:
request_entry = RevokeRequest.query.filter(
RevokeRequest.id == request_id).first()
if not request_entry:
return False
db_cert = Certificate.query.filter(Certificate.serial == serial).first()
if db_cert:
cert_file = os.path.join(app.config.cert_dir, db_cert.filename)
if os.path.exists(cert_file):
# XXX - This sequence should not fail
# Call update CRL file right now
app.config.ca.revokeCertificate(cert_file, update_crl=True)
db_cert.status = CERT_STATUS_REVOKED
if request_entry:
request_entry.status = CERT_STATUS_VALIDATED
rev = Revoke(comment="Validated from User request id=%s" % request_id,
serial=serial,
revoke_date=datetime.utcnow(),
revoke_request_id=request_id)
db.session.add(rev)
db.session.commit()
return True
return False
def getFilenameFromHash(self, hash_name):
cert = Certificate.query.filter(
Certificate.status == CERT_STATUS_VALIDATED
).filter(Certificate.name == hash_name).first()
if cert:
return cert.filename
return ""
def getCertificateList(self, with_cacerts=True):
ca_cert = app.config.ca.freadX509(app.config.ca.certificate)
if with_cacerts:
data_list = [
{
'index': 1,
'filename': os.path.basename(app.config.ca.certificate),
'name': os.path.basename(app.config.ca.certificate),
'cn': ca_cert.get_subject().CN,
'expiration_date': datetime.strptime(ca_cert.get_notAfter(),"%Y%m%d%H%M%SZ"),
},
{
'index': 2,
'filename': os.path.basename(app.config.ca.ca_crl),
'name': os.path.basename(app.config.ca.ca_crl),
'cn': "Certificate Revocation List",
'expiration_date': '---',
}
]
index = 3
else:
data_list = []
index = 1
signed_cert_list = Certificate.query.filter(
Certificate.status == CERT_STATUS_VALIDATED
).all()
for signed_cert in signed_cert_list:
data_list.append({
'index': index,
'serial': signed_cert.serial,
'filename': signed_cert.filename,
'name': signed_cert.name,
'cn': signed_cert.common_name,
'expiration_date': signed_cert.expire_after,
'start_date': signed_cert.start_before,
})
index += 1
return data_list
def getRevokedCertificateList(self):
index = 1
data_list = []
revoked_cert_list = Certificate.query.filter(
Certificate.status == CERT_STATUS_REVOKED
).all()
for revoked_cert in revoked_cert_list:
data_list.append({
'index': index,
'serial': revoked_cert.serial,
'filename': revoked_cert.filename,
'name': revoked_cert.name,
'cn': revoked_cert.common_name,
'expiration_date': revoked_cert.expire_after,
'start_date': revoked_cert.start_before,
})
index += 1
return data_list
def getRevocationRequestList(self):
index = 1
data_list = []
revocation_list = RevokeRequest.query.order_by(
RevokeRequest.status.asc()
).all()
for request in revocation_list:
frozen = False
if request.status != CERT_STATUS_PENDING:
frozen = True
data_list.append({
'index': index,
'serial': request.serial,
'filename': request.filename,
'reason': request.reason,
'cn': request.common_name,
'request_date': request.request_date,
'id': request.id,
'status': request.status,
'frozen': frozen,
})
index += 1
return data_list
def getCertificateRequestList(self):
# XXX all files in app.config.req_dir folder are csr request from users
data_list = []
index = 1
for file in os.listdir(app.config.req_dir):
csr_file = os.path.join(app.config.req_dir, file)
csr = app.config.ca.freadCertificateRequest(csr_file)
data_list.append({
'index': index,
'filename': file,
'filename_quote': urllib.quote(file),
'key': file.rstrip('.csr.pem'),
'cn': csr.get_subject().CN,
'o': csr.get_subject().O,
'ou': csr.get_subject().OU,
'l': csr.get_subject().L,
'st': csr.get_subject().ST,
'c': csr.get_subject().C,
'request_date': time.ctime(os.path.getmtime(csr_file))
})
index += 1
return data_list
def countCert(self):
return Certificate.query.filter(
Certificate.status == CERT_STATUS_VALIDATED
).count()
def countCsr(self):
return len(os.listdir(app.config.req_dir))
def countRevokedCert(self):
return Certificate.query.filter(
Certificate.status == CERT_STATUS_REVOKED
).count()
def countRevocationRequest(self):
return RevokeRequest.query.filter(
RevokeRequest.status == CERT_STATUS_PENDING
).count()
def getSerialToInt(self, cert):
return '{0:x}'.format(int(cert.get_serial_number()))
This diff is collapsed.
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