mirror of
https://github.com/laurivosandi/certidude
synced 2024-12-22 08:15:18 +00:00
Initial commit
This commit is contained in:
parent
6728f4131c
commit
0af381fc46
55
.gitignore
vendored
Normal file
55
.gitignore
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.goutputstream*
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
3
MANIFEST.in
Normal file
3
MANIFEST.in
Normal file
@ -0,0 +1,3 @@
|
||||
include README.rst
|
||||
include certidude/templates/*.html
|
||||
include certidude/templates/*.svg
|
34
README.rst
Normal file
34
README.rst
Normal file
@ -0,0 +1,34 @@
|
||||
Certidude
|
||||
=========
|
||||
|
||||
Certidude is a novel X.509 Certificate Authority management tool aiming to
|
||||
support PKCS#11 and in far future WebCrypto
|
||||
|
||||
Install
|
||||
-------
|
||||
|
||||
To install Certidude:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
apt-get install python3-openssl
|
||||
pip3 install certidude
|
||||
|
||||
|
||||
Setting up CA
|
||||
--------------
|
||||
|
||||
Certidude can set up CA relatively easily:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
certidude ca create /path/to/directory
|
||||
|
||||
Tweak command-line options until you meet your requirements and
|
||||
finally insert corresponding segment to your /etc/ssl/openssl.cnf
|
||||
|
||||
Finally serve the certificate authority via web:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
certidude serve
|
0
certidude/__init__.py
Normal file
0
certidude/__init__.py
Normal file
196
certidude/api.py
Normal file
196
certidude/api.py
Normal file
@ -0,0 +1,196 @@
|
||||
import re
|
||||
import falcon
|
||||
import os
|
||||
import json
|
||||
import types
|
||||
from datetime import datetime, date
|
||||
from OpenSSL import crypto
|
||||
from jinja2 import Environment, PackageLoader
|
||||
env = Environment(loader=PackageLoader('certidude', 'templates'))
|
||||
|
||||
RE_HOSTNAME = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$"
|
||||
|
||||
def omit(**kwargs):
|
||||
return dict([(key,value) for (key, value) in kwargs.items() if value])
|
||||
|
||||
def pop_certificate_authority(func):
|
||||
def wrapped(self, req, resp, *args, **kwargs):
|
||||
kwargs["ca"] = self.config.instantiate_authority(kwargs["ca"])
|
||||
return func(self, req, resp, *args, **kwargs)
|
||||
return wrapped
|
||||
|
||||
def validate_common_name(func):
|
||||
def wrapped(*args, **kwargs):
|
||||
if not re.match(RE_HOSTNAME, kwargs["cn"]):
|
||||
raise falcon.HTTPBadRequest("Invalid CN", "Common name supplied with request didn't pass the validation regex")
|
||||
return func(*args, **kwargs)
|
||||
return wrapped
|
||||
|
||||
class MyEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
if isinstance(obj, datetime):
|
||||
return obj.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + "Z"
|
||||
if isinstance(obj, date):
|
||||
return obj.strftime('%Y-%m-%d')
|
||||
if isinstance(obj, map):
|
||||
return tuple(obj)
|
||||
if isinstance(obj, types.GeneratorType):
|
||||
return tuple(obj)
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
|
||||
def serialize(func):
|
||||
"""
|
||||
Falcon response serialization
|
||||
"""
|
||||
def wrapped(instance, req, resp, **kwargs):
|
||||
assert not req.get_param("unicode") or req.get_param("unicode") == u"✓", "Unicode sanity check failed"
|
||||
resp.set_header("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
resp.set_header("Pragma", "no-cache");
|
||||
resp.set_header("Expires", "0");
|
||||
r = func(instance, req, resp, **kwargs)
|
||||
if not resp.body:
|
||||
if not req.client_accepts_json:
|
||||
raise falcon.HTTPUnsupportedMediaType(
|
||||
'This API only supports the JSON media type.',
|
||||
href='http://docs.examples.com/api/json')
|
||||
resp.set_header('Content-Type', 'application/json')
|
||||
resp.body = json.dumps(r, cls=MyEncoder)
|
||||
return r
|
||||
return wrapped
|
||||
|
||||
def templatize(path):
|
||||
template = env.get_template(path)
|
||||
def wrapper(func):
|
||||
def wrapped(instance, req, resp, **kwargs):
|
||||
assert not req.get_param("unicode") or req.get_param("unicode") == u"✓", "Unicode sanity check failed"
|
||||
r = func(instance, req, resp, **kwargs)
|
||||
if not resp.body:
|
||||
if req.get_header("Accept") == "application/json":
|
||||
resp.set_header("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||
resp.set_header("Pragma", "no-cache");
|
||||
resp.set_header("Expires", "0");
|
||||
resp.set_header('Content-Type', 'application/json')
|
||||
resp.body = json.dumps(r, cls=MyEncoder)
|
||||
return r
|
||||
else:
|
||||
resp.set_header('Content-Type', 'text/html')
|
||||
resp.body = template.render(request=req, **r)
|
||||
return r
|
||||
return wrapped
|
||||
return wrapper
|
||||
|
||||
class CertificateAuthorityBase(object):
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
|
||||
class SignedCertificateDetailResource(CertificateAuthorityBase):
|
||||
@pop_certificate_authority
|
||||
@validate_common_name
|
||||
def on_get(self, req, resp, ca, cn):
|
||||
path = os.path.join(ca.signed_dir, cn + ".pem")
|
||||
if not os.path.exists(path):
|
||||
raise falcon.HTTPNotFound()
|
||||
resp.stream = open(path, "rb")
|
||||
resp.append_header("Content-Disposition", "attachment; filename=%s.crt" % cn)
|
||||
|
||||
@pop_certificate_authority
|
||||
@validate_common_name
|
||||
def on_delete(self, req, resp, ca, cn):
|
||||
ca.revoke(cn)
|
||||
|
||||
class SignedCertificateListResource(CertificateAuthorityBase):
|
||||
@serialize
|
||||
@pop_certificate_authority
|
||||
@validate_common_name
|
||||
def on_get(self, req, resp, ca):
|
||||
for j in authority.get_signed():
|
||||
yield omit(
|
||||
key_type=j.key_type(),
|
||||
key_length=j.key_length(),
|
||||
subject=j.get_dn(),
|
||||
issuer=j.get_issuer_dn(),
|
||||
cn=j.subject.CN,
|
||||
c=j.subject.C,
|
||||
st=j.subject.ST,
|
||||
l=j.subject.L,
|
||||
o=j.subject.O,
|
||||
ou=j.subject.OU,
|
||||
fingerprint=j.get_pubkey_fingerprint())
|
||||
|
||||
class RequestDetailResource(CertificateAuthorityBase):
|
||||
@pop_certificate_authority
|
||||
@validate_common_name
|
||||
def on_get(self, req, resp, ca, cn):
|
||||
"""
|
||||
Fetch certificate signing request as PEM
|
||||
"""
|
||||
path = os.path.join(ca.request_dir, cn + ".pem")
|
||||
if not os.path.exists(path):
|
||||
raise falcon.HTTPNotFound()
|
||||
resp.stream = open(path, "rb")
|
||||
resp.append_header("Content-Disposition", "attachment; filename=%s.csr" % cn)
|
||||
|
||||
@pop_certificate_authority
|
||||
@validate_common_name
|
||||
def on_patch(self, req, resp, ca, cn):
|
||||
"""
|
||||
Sign a certificate signing request
|
||||
"""
|
||||
path = os.path.join(ca.request_dir, cn + ".pem")
|
||||
if not os.path.exists(path):
|
||||
raise falcon.HTTPNotFound()
|
||||
ca.sign(ca.get_request(cn))
|
||||
resp.body = "Certificate successfully signed"
|
||||
resp.status = falcon.HTTP_201
|
||||
resp.location = os.path.join(req.relative_uri, "..", "..", "signed", cn)
|
||||
|
||||
|
||||
class RequestListResource(CertificateAuthorityBase):
|
||||
@serialize
|
||||
@pop_certificate_authority
|
||||
def on_get(self, req, resp, ca):
|
||||
for j in ca.get_requests():
|
||||
yield omit(
|
||||
key_type=j.key_type(),
|
||||
key_length=j.key_length(),
|
||||
subject=j.get_dn(),
|
||||
cn=j.subject.CN,
|
||||
c=j.subject.C,
|
||||
st=j.subject.ST,
|
||||
l=j.subject.L,
|
||||
o=j.subject.O,
|
||||
ou=j.subject.OU,
|
||||
fingerprint=j.get_pubkey_fingerprint())
|
||||
|
||||
@pop_certificate_authority
|
||||
def on_post(self, req, resp, ca):
|
||||
|
||||
if req.get_header("Content-Type") != "application/pkcs10":
|
||||
raise falcon.HTTPUnsupportedMediaType(
|
||||
"This API call accepts only application/pkcs10 content type")
|
||||
|
||||
# POTENTIAL SECURITY HOLE HERE!
|
||||
# Should we sanitize input before we handle it to SSL libs?
|
||||
try:
|
||||
csr = crypto.load_certificate_request(
|
||||
crypto.FILETYPE_PEM, req.stream.read(req.content_length))
|
||||
except crypto.Error:
|
||||
raise falcon.HTTPBadRequest("Invalid CSR", "Failed to parse request body as PEM")
|
||||
|
||||
common_name = csr.get_subject().CN
|
||||
|
||||
if not re.match(RE_HOSTNAME, common_name):
|
||||
raise falcon.HTTPBadRequest("Invalid CN", "Common name supplied with CSR did not match validation regex")
|
||||
|
||||
path = os.path.join(ca.request_dir, common_name + ".pem")
|
||||
with open(path, "wb") as fh:
|
||||
fh.write(crypto.dump_certificate_request(
|
||||
crypto.FILETYPE_PEM, csr))
|
||||
|
||||
class CertificateAuthorityResource(CertificateAuthorityBase):
|
||||
@templatize("index.html")
|
||||
def on_get(self, req, resp, ca):
|
||||
return {
|
||||
"authority": self.config.instantiate_authority(ca)}
|
||||
|
||||
|
230
certidude/cli.py
Executable file
230
certidude/cli.py
Executable file
@ -0,0 +1,230 @@
|
||||
#!/usr/bin/python3
|
||||
# coding: utf-8
|
||||
|
||||
import socket
|
||||
import click
|
||||
import os
|
||||
import time
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
from OpenSSL import crypto
|
||||
from certidude.wrappers import CertificateAuthorityConfig, \
|
||||
CertificateAuthority, SerialCounter, Certificate, subject2dn
|
||||
|
||||
# Big fat warning:
|
||||
# m2crypto overflows around 2030 because on 32-bit systems
|
||||
# m2crypto does not support hardware engine support (?)
|
||||
# m2crypto CRL object is pretty much useless
|
||||
# pyopenssl has no straight-forward methods for getting RSA key modulus
|
||||
|
||||
# http://www.mad-hacking.net/documentation/linux/security/ssl-tls/creating-ca.xml
|
||||
|
||||
config = CertificateAuthorityConfig("/etc/ssl/openssl.cnf")
|
||||
|
||||
NOW = datetime.utcnow().replace(tzinfo=None)
|
||||
|
||||
|
||||
@click.command("create", help="Set up Certificate Authority in a directory")
|
||||
@click.option("--parent", "-p", help="Parent CA, none by default")
|
||||
@click.option("--common-name", "-cn", default=socket.gethostname(), help="Common name, hostname by default")
|
||||
@click.option("--country", "-c", default="ee", help="Country, Estonia by default")
|
||||
@click.option("--state", "-s", default="Harjumaa", help="State or country, Harjumaa by default")
|
||||
@click.option("--locality", "-l", default="Tallinn", help="City or locality, Tallinn by default")
|
||||
@click.option("--lifetime", default=20, help="Lifetime in years")
|
||||
@click.option("--organization", "-o", default="Example LLC", help="Company or organization name")
|
||||
@click.option("--organizational-unit", "-ou", default="Certification Department")
|
||||
@click.option("--crl-age", default=1, help="CRL expiration age, 1 day by default")
|
||||
@click.option("--pkcs11", default=False, is_flag=True, help="Use PKCS#11 token instead of files")
|
||||
@click.argument("directory")
|
||||
def ca_create(parent, country, state, locality, organization, organizational_unit, common_name, directory, crl_age, lifetime, pkcs11):
|
||||
click.echo("Generating 4096-bit RSA key...")
|
||||
|
||||
if pkcs11:
|
||||
raise NotImplementedError("Hardware token support not yet implemented!")
|
||||
else:
|
||||
key = crypto.PKey()
|
||||
key.generate_key(crypto.TYPE_RSA, 4096)
|
||||
slug = os.path.basename(directory)
|
||||
crl_distribution_points = "URI:http://%s/api/%s/revoked/" % (common_name, slug)
|
||||
ca = crypto.X509()
|
||||
ca.set_version(3)
|
||||
ca.set_serial_number(1)
|
||||
ca.get_subject().CN = common_name
|
||||
ca.get_subject().C = country
|
||||
ca.get_subject().ST = state
|
||||
ca.get_subject().L = locality
|
||||
ca.get_subject().O = organization
|
||||
ca.get_subject().OU = organizational_unit
|
||||
ca.gmtime_adj_notBefore(0)
|
||||
ca.gmtime_adj_notAfter(lifetime * 365 * 24 * 60 * 60)
|
||||
ca.set_issuer(ca.get_subject())
|
||||
ca.set_pubkey(key)
|
||||
ca.add_extensions([
|
||||
crypto.X509Extension(
|
||||
b"basicConstraints",
|
||||
True,
|
||||
b"CA:TRUE, pathlen:0"),
|
||||
crypto.X509Extension(
|
||||
b"keyUsage",
|
||||
True,
|
||||
b"keyCertSign, cRLSign"),
|
||||
crypto.X509Extension(
|
||||
b"subjectKeyIdentifier",
|
||||
False,
|
||||
b"hash",
|
||||
subject = ca),
|
||||
crypto.X509Extension(
|
||||
b"crlDistributionPoints",
|
||||
False,
|
||||
crl_distribution_points.encode("ascii"))
|
||||
])
|
||||
|
||||
click.echo("Signing %s..." % subject2dn(ca.get_subject()))
|
||||
ca.sign(key, "sha1")
|
||||
|
||||
if not os.path.exists(directory):
|
||||
os.makedirs(directory)
|
||||
for subdir in ("signed", "requests", "revoked"):
|
||||
if not os.path.exists(os.path.join(directory, subdir)):
|
||||
os.mkdir(os.path.join(directory, subdir))
|
||||
with open(os.path.join(directory, "ca_key.pem"), "wb") as fh:
|
||||
fh.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key))
|
||||
with open(os.path.join(directory, "ca_crt.pem"), "wb") as fh:
|
||||
fh.write(crypto.dump_certificate(crypto.FILETYPE_PEM, ca))
|
||||
with open(os.path.join(directory, "ca_crl.pem"), "wb") as fh:
|
||||
crl = crypto.CRL()
|
||||
fh.write(crl.export(ca, key, days=crl_age))
|
||||
with open(os.path.join(directory, "serial"), "w") as fh:
|
||||
fh.write("1")
|
||||
|
||||
|
||||
click.echo()
|
||||
click.echo("Add following to your /etc/ssl/openssl.cnf:")
|
||||
click.echo()
|
||||
click.echo("[CA_%s]" % slug)
|
||||
click.echo("dir = %s" % directory)
|
||||
click.echo("private_key = $dir/ca_key.pem")
|
||||
click.echo("certificate = $dir/ca_crt.pem")
|
||||
click.echo("new_certs_dir = $dir/requests/")
|
||||
click.echo("revoked_certs_dir = $dir/revoked/")
|
||||
click.echo("certs = $dir/signed/")
|
||||
click.echo("crl = $dir/ca_crl.pem")
|
||||
click.echo("serial = $dir/serial")
|
||||
click.echo("crlDistributionPoints = %s" % crl_distribution_points)
|
||||
|
||||
click.echo()
|
||||
click.echo("Use following commands to inspect the newly created files:")
|
||||
click.echo()
|
||||
click.echo(" openssl crl -inform PEM -text -noout -in %s" % os.path.join(directory, "ca_crl.pem"))
|
||||
click.echo(" openssl x509 -in %s -text -noout" % os.path.join(directory, "ca_crt.pem"))
|
||||
click.echo(" openssl rsa -in %s -check" % os.path.join(directory, "ca_key.pem"))
|
||||
click.echo()
|
||||
click.echo("Use following command to serve CA read-only:")
|
||||
click.echo()
|
||||
click.echo(" certidude serve")
|
||||
|
||||
@click.command("list", help="List Certificate Authorities")
|
||||
def ca_list():
|
||||
for ca in config.all_authorities():
|
||||
click.echo("Certificate authority '%s'" % ca.certificate.get_dn())
|
||||
|
||||
if ca.certificate.not_before < NOW and ca.certificate.not_after > NOW:
|
||||
click.echo(" ✓ Certificate valid %s" % (ca.certificate.not_after - NOW))
|
||||
elif NOW > ca.certificate.not_after:
|
||||
click.echo(" ✗ Certificate expired")
|
||||
else:
|
||||
click.echo(" ✗ Certificate authority not valid yet")
|
||||
|
||||
if os.path.exists(ca.private_key):
|
||||
click.echo(" ✓ Private key %s okay" % ca.private_key)
|
||||
else:
|
||||
click.echo(" ✗ Private key %s does not exist" % ca.private_key)
|
||||
|
||||
if os.path.isdir(ca.signed_dir):
|
||||
click.echo(" ✓ Signed certificates directory %s okay" % ca.signed_dir)
|
||||
else:
|
||||
click.echo(" ✗ Signed certificates directory %s okay" % ca.signed_dir)
|
||||
|
||||
click.echo(" Revoked certificates directory: %s" % ca.revoked_dir)
|
||||
click.echo(" Revocation list: %s" % ca.revocation_list)
|
||||
|
||||
click.echo()
|
||||
|
||||
@click.command("list", help="List Certificate Authorities")
|
||||
@click.argument("ca")
|
||||
@config.pop_certificate_authority()
|
||||
def cert_list(ca):
|
||||
mapping = {}
|
||||
|
||||
click.echo("Listing certificates for: %s" % ca.certificate.subject.CN)
|
||||
|
||||
for serial, reason, timestamp in ca.get_revoked():
|
||||
mapping[serial] = None, reason
|
||||
|
||||
for certificate in ca.get_signed():
|
||||
mapping[certificate.serial] = certificate, None
|
||||
|
||||
for serial, (certificate, reason) in sorted(mapping.items(), key=lambda j:j[0]):
|
||||
if not reason:
|
||||
click.echo(" %03d. %s %s" % (serial, certificate.subject.CN, (certificate.not_after-NOW)))
|
||||
else:
|
||||
click.echo(" %03d. Revoked due to: %s" % (serial, reason))
|
||||
|
||||
for request in ca.get_requests():
|
||||
click.echo(" ⌛ %s" % request.subject.CN)
|
||||
|
||||
@click.command("serve", help="Run built-in HTTP server")
|
||||
@click.option("-u", "--user", default=None, help="Run as user")
|
||||
@click.option("-p", "--port", default=80, help="Listen port")
|
||||
@click.option("-l", "--listen", default="0.0.0.0", help="Listen address")
|
||||
@click.option("-s", "--enable-signature", default=False, is_flag=True, help="Allow signing operations with private key of CA")
|
||||
def serve(user, port, listen, enable_signature):
|
||||
click.echo("Serving API at %s:%d" % (listen, port))
|
||||
import pwd
|
||||
import falcon
|
||||
from wsgiref.simple_server import make_server, WSGIServer
|
||||
from socketserver import ThreadingMixIn
|
||||
from certidude.api import CertificateAuthorityResource, \
|
||||
RequestDetailResource, RequestListResource, \
|
||||
SignedCertificateDetailResource, SignedCertificateListResource
|
||||
|
||||
class ThreadingWSGIServer(ThreadingMixIn, WSGIServer):
|
||||
pass
|
||||
click.echo("Listening on %s:%d" % (listen, port))
|
||||
|
||||
app = falcon.API()
|
||||
app.add_route("/api/{ca}/signed/{cn}/", SignedCertificateDetailResource(config))
|
||||
app.add_route("/api/{ca}/signed/", SignedCertificateListResource(config))
|
||||
app.add_route("/api/{ca}/request/{cn}/", RequestDetailResource(config))
|
||||
app.add_route("/api/{ca}/request/", RequestListResource(config))
|
||||
app.add_route("/api/{ca}/", CertificateAuthorityResource(config))
|
||||
httpd = make_server(listen, port, app, ThreadingWSGIServer)
|
||||
if user:
|
||||
_, _, uid, gid, gecos, root, shell = pwd.getpwnam(user)
|
||||
if uid == 0:
|
||||
click.echo("Please specify unprivileged user")
|
||||
exit(254)
|
||||
click.echo("Switching to user %s (uid=%d, gid=%d)" % (user, uid, gid))
|
||||
os.setgid(gid)
|
||||
os.setuid(uid)
|
||||
elif os.getuid() == 0:
|
||||
click.echo("Warning: running as root, this is not reccommended!")
|
||||
httpd.serve_forever()
|
||||
|
||||
@click.group(help="Certificate Authority management")
|
||||
def ca(): pass
|
||||
|
||||
@click.group(help="Certificate management")
|
||||
def cert(): pass
|
||||
|
||||
cert.add_command(cert_list)
|
||||
ca.add_command(ca_create)
|
||||
ca.add_command(ca_list)
|
||||
|
||||
@click.group()
|
||||
def entry_point(): pass
|
||||
|
||||
entry_point.add_command(ca)
|
||||
entry_point.add_command(cert)
|
||||
entry_point.add_command(serve)
|
35
certidude/templates/iconmonstr-certificate-15-icon.svg
Normal file
35
certidude/templates/iconmonstr-certificate-15-icon.svg
Normal file
@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- License Agreement at http://iconmonstr.com/license/ -->
|
||||
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="48px" height="48px" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<path id="certificate-15" d="M374.021,384.08c-4.527,29.103-16.648,55.725-36.043,77.92c-1.125-7.912-4.359-15.591-7.428-21.727
|
||||
c-7.023,3.705-15.439,5.666-22.799,5.666c-1.559,0-3.102-0.084-4.543-0.268c20.586-21.459,30.746-43.688,33.729-73.294
|
||||
c4.828,1.341,10.697,2.046,18.072,2.046C362.119,379.285,364.918,382.319,374.021,384.08z M457.709,445.672
|
||||
c-20.553-21.425-30.596-43.755-33.596-73.327c-4.861,1.358-10.73,2.079-18.207,2.079c-7.107,4.895-10.074,7.93-18.994,9.639
|
||||
c4.527,29.12,16.648,55.742,36.027,77.938c1.123-7.912,4.359-15.591,7.426-21.727C439.133,444.9,449.795,446.678,457.709,445.672z
|
||||
M372.01,362.789c-12.088-8.482-9.473-7.678-24.426-7.628c-0.018,0-0.018,0-0.033,0c-6.221,0-11.752-3.872-13.631-9.572
|
||||
c-4.576-13.68-3.018-11.551-15.088-19.95c-5.18-3.57-7.174-9.907-5.264-15.456c4.695-13.612,4.695-10.997,0-24.677
|
||||
c-1.877-5.499,0.033-11.869,5.264-15.457c12.07-8.383,10.496-6.27,15.088-19.958c1.879-5.717,7.41-9.564,13.631-9.564
|
||||
c0.016,0,0.016,0,0.033,0c14.938,0.042,12.322,0.888,24.426-7.628c2.514-1.76,5.465-2.649,8.449-2.649s5.934,0.889,8.449,2.649
|
||||
c12.086,8.491,9.471,7.678,24.426,7.628c0.016,0,0.016,0,0.016,0c6.236,0,11.77,3.847,13.68,9.564
|
||||
c4.561,13.654,2.951,11.542,15.055,19.958c3.822,2.632,5.969,6.822,5.969,11.165c0,1.425-0.234,2.884-0.721,4.292
|
||||
c-4.678,13.612-4.678,10.997,0,24.677c1.91,5.432,0,11.835-5.248,15.456c-12.104,8.399-10.494,6.287-15.055,19.95
|
||||
c-3.52,10.562-11.266,9.522-20.25,9.522c-7.947,0-7.98,0.721-17.871,7.678C383.879,366.326,377.039,366.326,372.01,362.789z
|
||||
M380.459,331.641c18.676,0,33.797-15.154,33.797-33.797c0-18.676-15.121-33.797-33.797-33.797s-33.797,15.121-33.797,33.797
|
||||
C346.662,316.486,361.783,331.641,380.459,331.641z M300.225,354.508c-28.76,18.172-61.131,38.574-67.837,42.799
|
||||
c-0.737-13.261-5.649-25.6-14.216-35.792c-0.998-1.257-99.79-127.031-123.981-157.987c-19.044-24.358-1.039-50.352,21.106-50.352
|
||||
c29.078,0,40.662,37.887,15.348,54.3l19.967,25.515l138.247-78.122c23.975-17.712,30.73-50.436,15.691-76.119
|
||||
C294.156,61.014,274.91,50,254.348,50c-8.155,0-16.068,1.677-23.57,5.013L88.918,127.577C66.58,138.281,54.292,159.27,54.292,181.6
|
||||
c0,14.015,4.836,28.55,15.062,41.408c24.786,31.165,124.643,158.859,125.641,160.133c14.794,19.682,0.293,47.259-23.621,47.259
|
||||
c-16.974,0-26.019-12.104-28.608-22.447c-3.018-12.104,1.19-24.157,13.269-31.903l-19.58-25.028
|
||||
c-14.686,10.327-24.032,26.001-25.876,43.521C106.646,431.857,136.386,462,171.633,462c10.821,0,21.542-2.984,31.014-8.617
|
||||
l94.158-59.379C301.33,386.896,305.891,369.461,300.225,354.508z M243.25,84.057c3.487-1.635,7.401-2.49,11.315-2.49
|
||||
c9.909,0,18.577,5.23,23.161,14.007c5.801,11.073,4.191,27.3-10.193,35.548l-91.114,51.609c0-20.453-9.975-39.212-26.957-50.67
|
||||
L243.25,84.057z M277.35,191.642c5.139,6.32,16.891,20.729,29.613,36.336c5.969-9.019,14.736-15.817,25.062-19.245
|
||||
c-11.549-14.166-21.775-26.739-26.805-32.883L277.35,191.642z M227.81,329.729l49.288-27.963l-10.863-14.149l-49.145,28.5
|
||||
L227.81,329.729z M259.428,209.772l-86.042,50.52l10.712,13.596l86.288-50.662L259.428,209.772z M281.516,237.182l-86.429,50.905
|
||||
l10.713,13.597l86.679-51.048L281.516,237.182z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 3.5 KiB |
18
certidude/templates/iconmonstr-time-13-icon.svg
Normal file
18
certidude/templates/iconmonstr-time-13-icon.svg
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<!-- The icon can be used freely in both personal and commercial projects with no attribution required, but always appreciated.
|
||||
You may NOT sub-license, resell, rent, redistribute or otherwise transfer the icon without express written permission from iconmonstr.com -->
|
||||
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="48px" height="48px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve">
|
||||
<path id="time-13-icon" d="M361.629,172.206c15.555-19.627,24.121-44.229,24.121-69.273V50h-259.5v52.933
|
||||
c0,25.044,8.566,49.646,24.121,69.273l50.056,63.166c9.206,11.617,9.271,27.895,0.159,39.584l-50.768,65.13
|
||||
c-15.198,19.497-23.568,43.85-23.568,68.571V462h259.5v-53.343c0-24.722-8.37-49.073-23.567-68.571l-50.769-65.13
|
||||
c-9.112-11.689-9.047-27.967,0.159-39.584L361.629,172.206z M330.634,364.678c11.412,14.64,15.116,29.947,15.116,47.321h-11.096
|
||||
c-4.586-17.886-31.131-30.642-62.559-47.586c-6.907-3.724-6.096-10.373-6.096-15.205h-20c0,4.18,1.03,11.365-6.106,15.202
|
||||
c-32.073,17.249-58.274,29.705-62.701,47.589H166.25c0-17.261,3.645-32.605,15.115-47.321l50.769-65.13
|
||||
c7.109-9.12,11.723-19.484,13.866-30.22v13.38h20V269.33c2.144,10.734,6.758,21.098,13.866,30.218L330.634,364.678z
|
||||
M197.966,167.862l-16.245-20.5c-11.538-14.56-15.471-30.096-15.471-47.361h179.5c0,17.149-3.872,32.727-15.471,47.361l-16.245,20.5
|
||||
H197.966z M246,294.458h20v15h-20V294.458z M246,321.958h20v15h-20V321.958z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
204
certidude/templates/index.html
Normal file
204
certidude/templates/index.html
Normal file
@ -0,0 +1,204 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
|
||||
<link href='http://fonts.googleapis.com/css?family=Ubuntu+Mono' rel='stylesheet' type='text/css'>
|
||||
<link href='http://fonts.googleapis.com/css?family=Gentium' rel='stylesheet' type='text/css'>
|
||||
<link href="//fonts.googleapis.com/css?family=PT+Sans+Narrow" rel="stylesheet" type="text/css">
|
||||
<meta charset="utf-8"/>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
<title>Certidude server</title>
|
||||
<style type="text/css">
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button, .button {
|
||||
color: #000;
|
||||
float: right;
|
||||
border: 1pt solid #ccc;
|
||||
background-color: #eee;
|
||||
border-radius: 6px;
|
||||
margin: 2px;
|
||||
padding: 4px 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.monospace {
|
||||
font-family: 'Ubuntu Mono', monospace;
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
footer {
|
||||
display: block;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #44c;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: #aaf;
|
||||
}
|
||||
|
||||
html,body {
|
||||
margin: 0;
|
||||
padding: 0 0 1em 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #222;
|
||||
background-image: url('http://fc00.deviantart.net/fs71/i/2013/078/9/6/free_hexa_pattern_cc0_by_black_light_studio-d4ig12f.png');
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.comment {
|
||||
color: #aaf;
|
||||
}
|
||||
|
||||
table th, table td {
|
||||
border: 1px solid #ccc;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
h1, h2, th {
|
||||
font-family: 'Gentium';
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-size: 22pt;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18pt;
|
||||
}
|
||||
|
||||
h2 svg {
|
||||
position: relative;
|
||||
top: 16px;
|
||||
}
|
||||
|
||||
p, td, footer, li, button {
|
||||
font-family: 'PT Sans Narrow';
|
||||
font-size: 14pt;
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow: auto;
|
||||
border: 1px solid #000;
|
||||
background: #444;
|
||||
color: #fff;
|
||||
font-size: 12pt;
|
||||
padding: 4px;
|
||||
border-radius: 6px;
|
||||
margin: 0 0;
|
||||
}
|
||||
|
||||
#container {
|
||||
|
||||
margin: 1em;
|
||||
background: #fff;
|
||||
padding: 1em;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
border-color: #aaa;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 4px 0;
|
||||
padding: 4px 0;
|
||||
clear: both;
|
||||
border-top: 1px dashed #ccc;
|
||||
}
|
||||
|
||||
li .details {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
li:hover .details {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
|
||||
<h1>Submit signing request</h1>
|
||||
|
||||
{% set s = authority.certificate.subject %}
|
||||
|
||||
<p>To submit new certificate signing request:</p>
|
||||
<pre>
|
||||
export CN=$(hostname)
|
||||
openssl genrsa -out $CN.key 4096
|
||||
openssl req -new -sha256 -key $CN.key -out $CN.csr -subj "{% if s.C %}/C={{ s.C}}{% endif %}{% if s.ST %}/ST={{ s.ST}}{% endif %}{% if s.L %}/L={{s.L}}{% endif %}{% if s.O %}/O={{ s.O}}{% endif %}{% if s.OU %}/OU={{ s.OU}}{% endif %}/CN=$CN"
|
||||
curl -H "Content-Type: application/pkcs10" -X POST -d "$(cat $CN.csr)" {{ request.url }}/request/
|
||||
</pre>
|
||||
|
||||
<p>After signing the request</p>
|
||||
|
||||
<pre>
|
||||
curl -f {{ request.url }}/signed/$CN > $CN.crt
|
||||
</pre>
|
||||
|
||||
<h1>Pending requests</h1>
|
||||
|
||||
<ul>
|
||||
{% for j in authority.get_requests() %}
|
||||
<li>
|
||||
{% include 'iconmonstr-time-13-icon.svg' %}
|
||||
<span class="monospace">{{ j.get_dn() }}</span>
|
||||
<span class="monospace details" title="SHA-1 of public key">{{ j.get_pubkey_fingerprint().upper() }}</span>
|
||||
|
||||
<a class="button" href="/api/{{authority.slug}}/request/{{j.subject.CN}}/">Fetch</a>
|
||||
<button onClick="javascript:$.ajax({url:'/api/{{authority.slug}}/request/{{j.subject.CN}}/',type:'patch'});">Sign</button>
|
||||
<button>Delete</button>
|
||||
<br/>
|
||||
<span>{{ j.key_length() }}-bit {{ j.key_type() }}</span>
|
||||
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<h1>Signed certificates</h1>
|
||||
|
||||
<ul>
|
||||
{% for j in authority.get_signed() | sort | reverse %}
|
||||
<li>
|
||||
|
||||
{% include 'iconmonstr-certificate-15-icon.svg' %}
|
||||
{{ j.serial}} <span class="monospace">{{ j.get_dn() }}</span>
|
||||
<span class="monospace details" title="SHA-1 of public key">{{ j.get_pubkey_fingerprint() }}</span>
|
||||
|
||||
{{ j.key_length() }}-bit {{ j.key_type() }}
|
||||
<a class="button" href="/api/{{authority.slug}}/signed/{{j.subject.CN}}/">Fetch</a>
|
||||
<button onClick="javascript:$.ajax({url:'/api/{{authority.slug}}/signed/{{j.subject.CN}}/',type:'delete'});">Revoke</button>
|
||||
|
||||
{% for key, value in j.get_extensions() %}
|
||||
{{key}}={{value}},
|
||||
{% endfor %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<h1>Revoked certificates</h1>
|
||||
|
||||
<ul>
|
||||
{% for serial, reason, timestamp in authority.get_revoked() %}
|
||||
<li>{{ serial}} {{ reason }} {{ timestamp}} </li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
282
certidude/wrappers.py
Normal file
282
certidude/wrappers.py
Normal file
@ -0,0 +1,282 @@
|
||||
import os
|
||||
from OpenSSL import crypto
|
||||
from datetime import datetime
|
||||
import hashlib
|
||||
from Crypto.Util import asn1
|
||||
import re
|
||||
import itertools
|
||||
from configparser import RawConfigParser
|
||||
|
||||
# https://jamielinux.com/docs/openssl-certificate-authority/
|
||||
|
||||
def subject2dn(subject):
|
||||
bits = []
|
||||
for j in "C", "S", "L", "O", "OU", "CN":
|
||||
if getattr(subject, j, None):
|
||||
bits.append("/%s=%s" % (j, getattr(subject, j)))
|
||||
return "".join(bits)
|
||||
|
||||
class SerialCounter(object):
|
||||
def __init__(self, filename):
|
||||
self.path = filename
|
||||
with open(filename, "r") as fh:
|
||||
self.value = int(fh.read(), 16)
|
||||
|
||||
def increment(self):
|
||||
self.value += 1
|
||||
with open(self.path, "w") as fh:
|
||||
fh.write("%04x" % self.value)
|
||||
return self.value
|
||||
|
||||
class CertificateAuthorityConfig(object):
|
||||
"""
|
||||
Attempt to parse CA-s from openssl.cnf
|
||||
"""
|
||||
|
||||
def __init__(self, *args):
|
||||
self._config = RawConfigParser()
|
||||
for arg in args:
|
||||
self._config.readfp(itertools.chain(["[global]"], open(os.path.expanduser(arg))))
|
||||
|
||||
def instantiate_authority(self, slug):
|
||||
section = "CA_" + slug
|
||||
|
||||
dirs = dict([(key, self._config.get(section, key)
|
||||
if self._config.has_option(section, key) else "")
|
||||
for key in ("dir", "certificate", "crl", "certs", "new_certs_dir", "serial", "private_key", "revoked_certs_dir")])
|
||||
|
||||
# Variable expansion, eg $dir
|
||||
for key, value in dirs.items():
|
||||
if "$" in value:
|
||||
dirs[key] = re.sub(r'\$([a-z]+)', lambda m:dirs[m.groups()[0]], value)
|
||||
|
||||
dirs.pop("dir")
|
||||
return CertificateAuthority(slug, **dirs)
|
||||
|
||||
def all_authorities(self):
|
||||
for section in self._config:
|
||||
if section.startswith("CA_"):
|
||||
yield self.instantiate_authority(section[3:])
|
||||
|
||||
def pop_certificate_authority(self):
|
||||
def wrapper(func):
|
||||
def wrapped(*args, **kwargs):
|
||||
slug = kwargs.pop("ca")
|
||||
kwargs["ca"] = self.instantiate_authority(slug)
|
||||
return func(*args, **kwargs)
|
||||
return wrapped
|
||||
return wrapper
|
||||
|
||||
class CertificateBase:
|
||||
def get_issuer_dn(self):
|
||||
return subject2dn(self.issuer)
|
||||
|
||||
def get_dn(self):
|
||||
return subject2dn(self.subject)
|
||||
|
||||
def key_length(self):
|
||||
return self._obj.get_pubkey().bits()
|
||||
|
||||
def key_type(self):
|
||||
if self._obj.get_pubkey().type() == 6:
|
||||
return "RSA"
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_pubkey(self):
|
||||
pub_asn1=crypto.dump_privatekey(crypto.FILETYPE_ASN1, self._obj.get_pubkey())
|
||||
pub_der=asn1.DerSequence()
|
||||
pub_der.decode(pub_asn1)
|
||||
return pub_der[1]
|
||||
|
||||
def get_pubkey_hex(self):
|
||||
h = "%x" % self.get_pubkey()
|
||||
assert len(h) * 4 == self.key_length(), "%s is not %s" % (len(h)*4, self.key_length())
|
||||
return ":".join(re.findall("..", "%x" % self.get_pubkey()))
|
||||
|
||||
def get_pubkey_fingerprint(self):
|
||||
import binascii
|
||||
return ":".join(re.findall("..", hashlib.sha1(binascii.unhexlify("%x" % self.get_pubkey())).hexdigest()))
|
||||
|
||||
class Certificate(CertificateBase):
|
||||
def __init__(self, filename, authority=None):
|
||||
self.path = os.path.realpath(filename)
|
||||
try:
|
||||
self._obj = crypto.load_certificate(crypto.FILETYPE_PEM, open(filename).read())
|
||||
except crypto.Error:
|
||||
click.echo("Failed to parse certificate: %s" % filename)
|
||||
raise
|
||||
self.not_before = datetime.strptime(self._obj.get_notBefore().decode("ascii"), "%Y%m%d%H%M%SZ")
|
||||
self.not_after = datetime.strptime(self._obj.get_notAfter().decode("ascii"), "%Y%m%d%H%M%SZ")
|
||||
self.subject = self._obj.get_subject()
|
||||
self.issuer = self._obj.get_issuer()
|
||||
self.serial = self._obj.get_serial_number()
|
||||
self.authority = authority
|
||||
self.subject_key_identifier = None
|
||||
|
||||
def get_extensions(self):
|
||||
for i in range(1, self._obj.get_extension_count()):
|
||||
ext = self._obj.get_extension(i)
|
||||
yield ext.get_short_name(), str(ext)
|
||||
|
||||
def digest(self):
|
||||
return self._obj.digest("md5").decode("ascii")
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.serial == other.serial
|
||||
|
||||
def __gt__(self, other):
|
||||
return self.serial > other.serial
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.serial < other.serial
|
||||
|
||||
def __gte__(self, other):
|
||||
return self.serial >= other.serial
|
||||
|
||||
def __lte__(self, other):
|
||||
return self.serial <= other.serial
|
||||
|
||||
def lock_crl(func):
|
||||
def wrapped(ca, *args, **kwargs):
|
||||
# TODO: Implement actual locking!
|
||||
try:
|
||||
crl = crypto.load_crl(crypto.FILETYPE_PEM, open(ca.revocation_list).read())
|
||||
except crypto.Error:
|
||||
click.echo("Failed to parse CRL in %s" % ca.revocation_list)
|
||||
raise
|
||||
count = len(crl.get_revoked() or ())
|
||||
retval = func(ca, crl, *args, **kwargs)
|
||||
if count != len(crl.get_revoked() or ()):
|
||||
click.echo("Updating CRL")
|
||||
partial = ca.revocation_list + ".part"
|
||||
with open(partial, "wb") as fh:
|
||||
fh.write(crl.export(
|
||||
ca.certificate._obj,
|
||||
crypto.load_privatekey(crypto.FILETYPE_PEM, open(ca.private_key).read()),
|
||||
crypto.FILETYPE_PEM))
|
||||
os.rename(partial, ca.revocation_list)
|
||||
return retval
|
||||
return wrapped
|
||||
|
||||
class Request(CertificateBase):
|
||||
def __init__(self, request_path):
|
||||
self.path = request_path
|
||||
self._obj = crypto.load_certificate_request(crypto.FILETYPE_PEM, open(self.path).read())
|
||||
self.subject = self._obj.get_subject()
|
||||
|
||||
"""
|
||||
pub_asn1=crypto.dump_privatekey(crypto.FILETYPE_ASN1, self._obj.get_pubkey())
|
||||
pub_der=asn1.DerSequence()
|
||||
pub_der.decode(pub_asn1)
|
||||
n=pub_der[1]
|
||||
# Get the modulus
|
||||
print("public modulus: %x" % n)
|
||||
import binascii
|
||||
self.sha_hash = hashlib.sha1(binascii.unhexlify("%x" % n)).hexdigest()
|
||||
"""
|
||||
|
||||
def __repr__(self):
|
||||
return "Request(%s)" % repr(self.path)
|
||||
|
||||
class CertificateAuthority(object):
|
||||
def __init__(self, slug, certificate, crl, certs, new_certs_dir, revoked_certs_dir=None, serial=None, private_key=None):
|
||||
self.slug = slug
|
||||
self.revocation_list = crl
|
||||
self.signed_dir = certs
|
||||
self.request_dir = new_certs_dir
|
||||
self.revoked_dir = revoked_certs_dir
|
||||
self.private_key = private_key
|
||||
|
||||
if isinstance(certificate, str):
|
||||
self.certificate = Certificate(certificate, self)
|
||||
else:
|
||||
self.certificate = certificate
|
||||
|
||||
if isinstance(serial, str):
|
||||
self.serial_counter=SerialCounter(serial)
|
||||
else:
|
||||
self.serial_counter=serial
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
return "CertificateAuthority(slug=%s)" % repr(self.slug)
|
||||
|
||||
def get_request(self, cn):
|
||||
return Request(os.path.join(self.request_dir, cn + ".pem"))
|
||||
|
||||
def get_certificate(self, cn):
|
||||
return Certificate(os.path.join(self.signed_dir, cn + ".pem"))
|
||||
|
||||
@lock_crl
|
||||
def revoke(self, crl, cn):
|
||||
certificate = self.get_certificate(cn)
|
||||
revocation = crypto.Revoked()
|
||||
revocation.set_rev_date(datetime.now().strftime("%Y%m%d%H%M%SZ").encode("ascii"))
|
||||
revocation.set_reason(b"keyCompromise")
|
||||
revocation.set_serial(("%x" % certificate.serial).encode("ascii"))
|
||||
if self.revoked_dir:
|
||||
os.rename(certificate.path, self.revoked_dir)
|
||||
else:
|
||||
os.unlink(certificate.path)
|
||||
crl.add_revoked(revocation)
|
||||
|
||||
@lock_crl
|
||||
def get_revoked(self, crl):
|
||||
for revocation in crl.get_revoked() or ():
|
||||
yield int(revocation.get_serial(), 16), \
|
||||
revocation.get_reason().decode("ascii"), \
|
||||
datetime.strptime(revocation.get_rev_date().decode("ascii"), "%Y%m%d%H%M%SZ")
|
||||
|
||||
def get_signed(self):
|
||||
for root, dirs, files in os.walk(self.signed_dir):
|
||||
for filename in files:
|
||||
yield Certificate(os.path.join(root, filename))
|
||||
break
|
||||
|
||||
def get_requests(self):
|
||||
for root, dirs, files in os.walk(self.request_dir):
|
||||
for filename in files:
|
||||
yield Request(os.path.join(root, filename))
|
||||
|
||||
def sign(self, request, lifetime=5*365*24*60*60):
|
||||
cert = crypto.X509()
|
||||
cert.add_extensions([
|
||||
crypto.X509Extension(
|
||||
b"basicConstraints",
|
||||
True,
|
||||
b"CA:FALSE, pathlen:0"),
|
||||
crypto.X509Extension(
|
||||
b"keyUsage",
|
||||
True,
|
||||
b"digitalSignature, keyEncipherment"),
|
||||
crypto.X509Extension(
|
||||
b"subjectKeyIdentifier",
|
||||
False,
|
||||
b"hash",
|
||||
subject = self.certificate._obj),
|
||||
crypto.X509Extension(
|
||||
b"authorityKeyIdentifier",
|
||||
False,
|
||||
b"keyid:always",
|
||||
issuer = self.certificate._obj)])
|
||||
cert.set_pubkey(request._obj.get_pubkey())
|
||||
cert.set_subject(request._obj.get_subject())
|
||||
cert.gmtime_adj_notBefore(0)
|
||||
cert.gmtime_adj_notAfter(lifetime)
|
||||
cert.set_serial_number(self.serial_counter.increment())
|
||||
|
||||
pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, open(self.private_key).read())
|
||||
cert.sign(pkey, 'sha1')
|
||||
|
||||
path = os.path.join(self.signed_dir, request.subject.CN + ".pem")
|
||||
assert not os.path.exists(path), "File %s already exists!" % path
|
||||
|
||||
buf = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
|
||||
with open(path + ".part", "wb") as fh:
|
||||
fh.write(buf)
|
||||
os.rename(path + ".part", path)
|
||||
click.echo("Wrote certififcate to: %s" % path)
|
||||
os.unlink(request.path)
|
||||
click.echo("Deleted request: %s" % request.path)
|
||||
|
6
misc/certidude
Normal file
6
misc/certidude
Normal file
@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from certidude.cli import entry_point
|
||||
|
||||
if __name__ == "__main__":
|
||||
entry_point()
|
44
setup.py
Normal file
44
setup.py
Normal file
@ -0,0 +1,44 @@
|
||||
#!/usr/bin/python
|
||||
# coding: utf-8
|
||||
import os
|
||||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name = "certidude",
|
||||
version = "0.1.2",
|
||||
author = u"Lauri Võsandi",
|
||||
author_email = "lauri.vosandi@gmail.com",
|
||||
description = "Certidude is a novel X.509 Certificate Authority management tool aiming to support PKCS#11 and in far future WebCrypto.",
|
||||
license = "MIT",
|
||||
keywords = "falcon http jinja2 x509 pkcs11 webcrypto",
|
||||
url = "http://github.com/laurivosandi/certidude",
|
||||
packages=[
|
||||
"certidude",
|
||||
],
|
||||
long_description=open("README.rst").read(),
|
||||
install_requires=[
|
||||
"click",
|
||||
"falcon",
|
||||
"jinja2"
|
||||
],
|
||||
scripts=[
|
||||
"misc/certidude"
|
||||
],
|
||||
include_package_data = True,
|
||||
package_data={
|
||||
"certidude": ["certidude/templates/*.html"],
|
||||
},
|
||||
classifiers=[
|
||||
"Development Status :: 4 - Beta",
|
||||
"Environment :: Console",
|
||||
"Intended Audience :: Developers",
|
||||
"Intended Audience :: System Administrators",
|
||||
"License :: Freely Distributable",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Natural Language :: English",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
],
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user