Commit 0eff0c1f authored by Alain Takoudjou's avatar Alain Takoudjou

certificate_authority: improve the first version, add extensions to crs

parent 1f2862fb
......@@ -49,6 +49,8 @@ setup(name=name,
'PyRSS2Gen',
'dnspython',
'pyOpenSSL', # manage ssl certificates
'pyasn1', # ASN.1 types and codecs for certificates
'pyasn1-modules',
'requests', # http requests
] + additional_install_requires,
extras_require = {
......
......@@ -5,13 +5,15 @@ import subprocess
import time
import ConfigParser
import uuid
import ssl
import logging
import cgi, errno
import urlparse
import errno
# import urlparse
from OpenSSL import crypto, SSL
import requests
import traceback
from pyasn1.codec.der import encoder as der_encoder
from pyasn1.type import tag
from pyasn1_modules import rfc2459
def popenCommunicate(command_list, input=None):
......@@ -27,9 +29,140 @@ def popenCommunicate(command_list, input=None):
command_list, result))
return result
class GeneralNames(rfc2459.GeneralNames):
"""
rfc2459 has wrong tagset.
"""
tagSet = tag.TagSet(
(),
tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0),
)
class DistributionPointName(rfc2459.DistributionPointName):
"""
rfc2459 has wrong tagset.
"""
tagSet = tag.TagSet(
(),
tag.Tag(tag.tagClassContext, tag.tagFormatConstructed, 0),
)
class ExtensionType():
CRL_DIST_POINTS = "crlDistributionPoints"
BASIC_CONSTRAINTS = "basicConstraints"
KEY_USAGE = "keyUsage"
NS_CERT_TYPE = "nsCertType"
NS_COMMENT = "nsComment"
SUBJECT_KEY_ID = "subjectKeyIdentifier"
AUTH_KEY_ID = "authorityKeyIdentifier"
class X509Extension(object):
known_extension_list = [name for (attr, name) in vars(ExtensionType).items()
if attr.isupper()]
def setX509Extension(self, ext_type, critical, value, subject=None, issuer=None):
if not ext_type in self.known_extension_list:
raise ValueError('Extension type is not known from ExtensionType class')
if ext_type == ExtensionType.CRL_DIST_POINTS:
cdp = self._getCrlDistPointExt(value)
return crypto.X509Extension(
b'%s' % ext_type,
critical,
'DER:' + cdp.encode('hex'),
subject=subject,
issuer=issuer,
)
else:
return crypto.X509Extension(
ext_type,
critical,
value,
subject=subject,
issuer=issuer,
)
def _getCrlDistPointExt(self, cdp_list):
cdp = rfc2459.CRLDistPointsSyntax()
position = 0
for cdp_type, cdp_value in cdp_list:
cdp_entry = rfc2459.DistributionPoint()
general_name = rfc2459.GeneralName()
if not cdp_type in ['dNSName', 'directoryName', 'uniformResourceIdentifier']:
raise ValueError("crlDistributionPoints GeneralName '%s' is not valid" % cdp_type)
general_name.setComponentByName(cdp_type, cdp_value)
general_names = GeneralNames()
general_names.setComponentByPosition(0, general_name)
name = DistributionPointName()
name.setComponentByName('fullName', general_names)
cdp_entry.setComponentByName('distributionPoint', name)
cdp.setComponentByPosition(position, cdp_entry)
position += 1
return der_encoder.encode(cdp)
def setCaExtensions(self, cert_obj, issuer=None):
"""
extensions for default certificate
"""
cert_obj.add_extensions([
self.setX509Extension(ExtensionType.BASIC_CONSTRAINTS, True,
"CA:TRUE, pathlen:0"),
self.setX509Extension(ExtensionType.NS_COMMENT,
False, "OpenSSL CA Certificate"),
self.setX509Extension(ExtensionType.KEY_USAGE,
True, "keyCertSign, cRLSign"),
self.setX509Extension(ExtensionType.SUBJECT_KEY_ID,
False, "hash", subject=cert_obj),
])
cert_obj.add_extensions([
self.setX509Extension(ExtensionType.AUTH_KEY_ID,
False, "keyid:always,issuer", issuer=cert_obj)
])
def setDefaultExtensions(self, cert_obj, subject=None, issuer=None, crl_url=None):
"""
extensions for default certificate
"""
cert_obj.add_extensions([
self.setX509Extension(ExtensionType.BASIC_CONSTRAINTS, False, "CA:FALSE"),
self.setX509Extension(ExtensionType.NS_COMMENT,
False, "OpenSSL Generated Certificate"),
self.setX509Extension(ExtensionType.SUBJECT_KEY_ID,
False, "hash", subject=subject),
])
cert_obj.add_extensions([
self.setX509Extension(ExtensionType.AUTH_KEY_ID,
False, "keyid,issuer", issuer=issuer)
])
if crl_url:
cert_obj.add_extensions([
self.setX509Extension(ExtensionType.CRL_DIST_POINTS,
False, [("uniformResourceIdentifier", crl_url)])
])
def setDefaultCsrExtensions(self, cert_obj, subject=None, issuer=None):
"""
extensions for certificate signature request
"""
cert_obj.add_extensions([
self.setX509Extension(ExtensionType.BASIC_CONSTRAINTS, False, "CA:FALSE"),
self.setX509Extension(ExtensionType.KEY_USAGE,
False, "nonRepudiation, digitalSignature, keyEncipherment"),
])
class CertificateBase(object):
def __init__(self):
self.X509Extension = X509Extension()
pass
def validateCertAndKey(self, cert_file, key_file):
......@@ -66,14 +199,15 @@ class CertificateBase(object):
key.generate_key(crypto.TYPE_RSA, size)
try:
key_fd = os.open(output_file, os.O_CREAT|os.O_WRONLY|os.O_EXCL)
key_fd = os.open(output_file,
os.O_CREAT|os.O_WRONLY|os.O_EXCL|os.O_TRUNC,
0640)
except OSError, e:
if e.errno != errno.EEXIST:
raise
else:
os.write(key_fd, crypto.dump_privatekey(crypto.FILETYPE_PEM, key))
os.close(key_fd)
os.chmod(output_file, 0640)
def generateCertificateRequest(self, key_file, output_file, cn,
country, state, locality='', email='', organization='',
......@@ -92,6 +226,7 @@ class CertificateBase(object):
subject.OU = organization_unit
subject.emailAddress = email
req.set_pubkey(key)
self.X509Extension.setDefaultCsrExtensions(req)
req.sign(key, digest)
with open(output_file, 'w') as req_file:
......@@ -109,8 +244,6 @@ class CertificateBase(object):
if not self.verifyCertificateChain(cert, [ca_cert]):
return False
if key_file:
#with open(key_file) as f_key:
# key = f_key.read()
return self.validateCertAndKey(cert_file, key_file)
return True
......@@ -148,15 +281,19 @@ class CertificateAuthority(CertificateBase):
def __init__(self, openssl_binary,
openssl_configuration=None, certificate='cacert.pem',
key='cakey.pem', ca_directory=None):
key='cakey.pem', crl='cacrl.pem', ca_directory=None):
self.key = key
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_configuration = openssl_configuration
self.ca_directory = ca_directory
if self.ca_directory is None:
self.ca_directory = os.getcwd()
self.ca_crl = crl
for file in ['crlnumber', 'serial']:
if not os.path.exists(os.path.join(self.ca_directory, file)):
with open(os.path.join(self.ca_directory, file), 'w') as f:
......@@ -201,10 +338,25 @@ class CertificateAuthority(CertificateBase):
if self.openssl_configuration is None:
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',
self.openssl_configuration, '-out', output_file,
'-infiles', csr_file])
def genCertificateRevocationList(self):
if self.openssl_configuration is None:
raise ValueError('Openssl configuration file not found!')
popenCommunicate([self.openssl_binary, 'ca', '-batch', '-config',
self.openssl_configuration, '-gencrl', '-out', self.ca_crl])
def revokeCertificate(self, cert_file):
if self.openssl_configuration is None:
raise ValueError('Openssl configuration file not found!')
popenCommunicate([self.openssl_binary, 'ca', '-batch', '-config',
self.openssl_configuration, '-revoke', cert_file])
class CertificateAuthorityRequest(CertificateBase):
......@@ -217,6 +369,7 @@ class CertificateAuthorityRequest(CertificateBase):
self.ca_url = ca_url
self.logger = logger
self.max_retry = max_retry
self.X509Extension = X509Extension()
if self.logger is None:
self.logger = logging.getLogger('Certificate Request')
......@@ -264,8 +417,12 @@ class CertificateAuthorityRequest(CertificateBase):
# sleep a bit then try again until ca cert is ready
time.sleep(10)
with open(self.cacertificate, 'w') as f:
f.write(response.text)
fd = os.open(self.cacertificate,
os.O_CREAT | os.O_EXCL | os.O_WRONLY | os.O_TRUNC, 0644)
try:
os.write(fd, response.text)
finally:
os.close(fd)
def signCertificateWeb(self, csr_file):
......@@ -326,11 +483,15 @@ class CertificateAuthorityRequest(CertificateBase):
with open(cert_temp, 'w') as cf:
cf.write(response.text)
os.chmod(cert_temp, 0640)
self.logger.info("Validating signed certificate...")
if self.checkCertificateValidity(self.cacertificate, cert_temp, self.key):
os.rename(cert_temp, self.certificate)
fd = os.open(self.certificate,
os.O_CREAT | os.O_EXCL | os.O_WRONLY | os.O_TRUNC, 0644)
try:
os.write(fd, response.text)
finally:
os.close(fd)
os.unlink(cert_temp)
else:
raise Exception("Error: Certificate validation failed. " \
"This signed certificate should be revoked!")
......
......@@ -35,6 +35,8 @@ def parseArguments():
help='Path for Certificate file. Defaul: $ca_dir/cacert.pem')
parser.add_argument('--key_file',
help='Path of key file. Default: $key_dir/cakey.pem')
parser.add_argument('--crl_file',
help='Path of Certificate Revocation List file. Default: $crl_dir/cacrl.pem')
parser.add_argument('--host',
default=[],
help='Host or IP of ca server.')
......@@ -101,11 +103,13 @@ def start():
options.cert_file = os.path.join(options.ca_dir, 'cacert.pem')
if not options.key_file:
options.key_file = os.path.join(options.ca_dir, 'private', 'cakey.pem')
if not options.crl_file:
options.crl_file = os.path.join(options.crl_dir, 'cacrl.pem')
logger = getLogger(options.debug, options.log_file)
ca = CertificateAuthority(options.openssl_bin,
openssl_configuration=options.config_file, certificate=options.cert_file,
key=options.key_file, ca_directory=options.ca_dir)
key=options.key_file, crl=options.crl_file, ca_directory=options.ca_dir)
#config = Config()
app.config.update(
......@@ -132,6 +136,8 @@ def start():
# Generate certificate Authority cert and key
ca.checkAuthority()
# XXX - maybe CRL must be generated from cron every xx hours
ca.genCertificateRevocationList()
app.logger.addHandler(logger)
app.logger.info("Certificate Authority server started on http://%s:%s" % (
......
......@@ -9,6 +9,9 @@ import sys
import prettytable
import glob
from slapos.certificate_authority.certificate_authority import CertificateBase
import urllib, urllib2, httplib, socket
CLIENT_IP = '::1'
def parseArguments():
"""
......@@ -28,9 +31,36 @@ def parseArguments():
parser.add_argument('--sign',
default=False, action="store_true",
help='Request sign certificate')
parser.add_argument('--host',
default='::1',
help='IPv4 or IPv6 host address to use for action sign')
return parser
class BindableHTTPConnection(httplib.HTTPConnection):
def connect(self):
"""Connect to the host and port specified in __init__."""
try:
socket.inet_aton(self.source_ip)
self.sock = socket.socket()
except socket.error:
self.sock = socket.socket(family=socket.AF_INET6)
self.sock.bind((self.source_ip, 0))
if isinstance(self.timeout, float):
self.sock.settimeout(self.timeout)
self.sock.connect((self.host,self.port))
def BindableHTTPConnectionFactory(source_ip):
def _get(host, port=None, strict=None, timeout=0):
bhc=BindableHTTPConnection(host, port=port, strict=strict, timeout=timeout)
bhc.source_ip=source_ip
return bhc
return _get
class BindableHTTPHandler(urllib2.HTTPHandler):
def http_open(self, req):
return self.do_open(BindableHTTPConnectionFactory(CLIENT_IP), req)
def main():
parser = parseArguments()
options = parser.parse_args()
......@@ -47,6 +77,8 @@ def main():
if options.list:
exit(listCertificateRequest(options))
elif options.sign and len(options.key_list) > 0:
global CLIENT_IP
CLIENT_IP = options.host
exit(requestSigncertificate(options))
parser.print_help()
......@@ -74,7 +106,9 @@ def listCertificateRequest(options):
return 0
def requestSigncertificate(options):
"""
Sign certificate locally, using web API
"""
req_directory = os.path.join(options.ca_dir, 'req')
code = 0
x509 = CertificateBase()
......@@ -87,14 +121,22 @@ def requestSigncertificate(options):
req = x509.freadCertificateRequest(csr_file)
cn = x509.getSubject(req)['CN']
data = {'key': key}
values = {'key': key}
logging.info("Signing %s..." % cn)
response = requests.post('%s/signcert' % options.ca_url, data=data)
if response.status_code != 200:
code = -1*response.status_code
logging.error("ERROR %s: Failed to sign certifice from %s.\n%s" % (
response.status_code, cn, response.text))
#response = requests.post('%s/signcert' % options.ca_url, data=data, headers={'host': options.host})
data = urllib.urlencode(values)
req = urllib2.Request('%s/signcert' % options.ca_url, data)
opener = urllib2.build_opener(BindableHTTPHandler)
try:
urllib2.install_opener(opener)
response = urllib2.urlopen(req)
except urllib2.HTTPError as e:
raise
#if response.status_code != 200:
# code = -1*response.status_code
# logging.error("ERROR %s: Failed to sign certifice from %s.\n%s" % (
# response.status_code, cn, response.text))
else:
logging.info("%s is signed, server responded with: %s" % (cn,
response.text))
response.read()))
return code
<!doctype html>
<title>Flaskr</title>
<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 -%}
<a href="/get/{{ filename }}">{{ filename }}</a>
<li><a href="/get/{{ filename }}">{{ filename }}</a></li>
{% endfor -%}
</ul>
</div>
\ No newline at end of file
<!doctype html>
<title>Certificate Authority Web</title>
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<!-- Optional theme -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
<!-- Latest compiled and minified JavaScript -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}">
<div class=page>
<nav class="navbar navbar-default navbar-fixed-top"></nav>
<div class="container">
</div> <!-- /container -->
{% block body %}{% endblock %}
</div>
\ No newline at end of file
......@@ -42,17 +42,25 @@ def writefile(path, content, mode=0640):
@app.route('/')
def index():
# page to list certificates, also connection link
cert_list = [os.path.basename(app.config.ca.certificate)]
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):
......
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