mirror of
https://github.com/laurivosandi/certidude
synced 2024-12-22 16:25:17 +00:00
Refactor
* Remove PyOpenSSL based wrapper classes * Remove unused API calls * Add certificate renewal via X-Renewal-Signature header * Remove (extended) key usage handling * Clean up OpenVPN and nginx server setup code * Use UDP port 51900 for OpenVPN by default * Add basic auth fallback for iOS in addition to Android * Reduce complexity
This commit is contained in:
parent
d1aa2f2073
commit
06010ceaf3
23
README.rst
23
README.rst
@ -93,13 +93,9 @@ TODO
|
|||||||
----
|
----
|
||||||
|
|
||||||
* `OCSP <https://tools.ietf.org/html/rfc4557>`_ support, needs a bit hacking since OpenSSL wrappers are not exposing the functionality.
|
* `OCSP <https://tools.ietf.org/html/rfc4557>`_ support, needs a bit hacking since OpenSSL wrappers are not exposing the functionality.
|
||||||
* `SECP <https://tools.ietf.org/html/draft-nourse-scep-23>`_ support, a client implementation available `here <https://github.com/certnanny/sscep>`_. Not sure if we can implement server-side events within current standard.
|
* `SCEP <https://tools.ietf.org/html/draft-nourse-scep-23>`_ support, a client implementation available `here <https://github.com/certnanny/sscep>`_. Not sure if we can implement server-side events within current standard.
|
||||||
* Deep mailbox integration, eg fetch CSR-s from mailbox via IMAP.
|
|
||||||
* WebCrypto support, meanwhile check out `hwcrypto.js <https://github.com/open-eid/hwcrypto.js>`_.
|
* WebCrypto support, meanwhile check out `hwcrypto.js <https://github.com/open-eid/hwcrypto.js>`_.
|
||||||
* Certificate push/pull, making it possible to sign offline.
|
* Ability to send OpenVPN profile URL tokens via e-mail, for simplified VPN adoption.
|
||||||
* PKCS#11 hardware token support for signatures at command-line.
|
|
||||||
* Ability to send ``.ovpn`` bundle URL tokens via e-mail, for simplified VPN adoption.
|
|
||||||
* Cronjob for deleting expired certificates
|
|
||||||
* Signer process logging.
|
* Signer process logging.
|
||||||
|
|
||||||
|
|
||||||
@ -384,15 +380,14 @@ as this information will already exist in AD and duplicating it in the certifica
|
|||||||
doesn't make sense. Additionally the information will get out of sync if
|
doesn't make sense. Additionally the information will get out of sync if
|
||||||
attributes are changed in AD but certificates won't be updated.
|
attributes are changed in AD but certificates won't be updated.
|
||||||
|
|
||||||
If machine is enrolled, eg by running certidude request:
|
If machine is enrolled, eg by running ``certidude request`` as root on Ubuntu/Fedora/Mac OS X:
|
||||||
|
|
||||||
* If Kerberos credentials are presented machine is automatically enrolled
|
* If Kerberos credentials are presented machine can be automatically enrolled depending on the ``machine enrollment`` setting
|
||||||
* Common name is set to short hostname/machine name in AD
|
* Common name is set to short ``hostname``
|
||||||
* E-mail is not filled in (maybe we can fill in something from AD?)
|
* It is tricky to determine user who is triggering the action so given name, surname and e-mail attributes are not filled in
|
||||||
* Given name and surname are not filled in
|
|
||||||
|
|
||||||
If user enrolls, eg by clicking generate bundle button in the web interface:
|
If user enrolls, eg by clicking generate bundle button in the web interface:
|
||||||
|
|
||||||
* Common name is either set to username or username@device-identifier depending on the 'user certificate enrollment' setting
|
* Common name is either set to ``username`` or ``username@device-identifier`` depending on the ``user enrollment`` setting
|
||||||
* Given name and surname are filled in based on LDAP attributes of the user
|
* Given name and surname are not filled in because Unicode characters cause issues in OpenVPN Connect app
|
||||||
* E-mail not filled in (should it be filled in? Can we even send mail to user if it's in external domain?)
|
* E-mail is not filled in because it might change in AD
|
||||||
|
@ -5,13 +5,14 @@ import mimetypes
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import click
|
import click
|
||||||
|
import hashlib
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from certidude import authority, mailer
|
from certidude import authority, mailer
|
||||||
from certidude.auth import login_required, authorize_admin
|
from certidude.auth import login_required, authorize_admin
|
||||||
from certidude.user import User
|
from certidude.user import User
|
||||||
from certidude.decorators import serialize, event_source, csrf_protection
|
from certidude.decorators import serialize, event_source, csrf_protection
|
||||||
from certidude.wrappers import Request, Certificate
|
from cryptography.x509.oid import NameOID
|
||||||
from certidude import const, config
|
from certidude import const, config
|
||||||
|
|
||||||
logger = logging.getLogger("api")
|
logger = logging.getLogger("api")
|
||||||
@ -44,6 +45,33 @@ class SessionResource(object):
|
|||||||
@login_required
|
@login_required
|
||||||
@event_source
|
@event_source
|
||||||
def on_get(self, req, resp):
|
def on_get(self, req, resp):
|
||||||
|
def serialize_requests(g):
|
||||||
|
for common_name, path, buf, obj, server in g():
|
||||||
|
yield dict(
|
||||||
|
common_name = common_name,
|
||||||
|
server = server,
|
||||||
|
md5sum = hashlib.md5(buf).hexdigest(),
|
||||||
|
sha1sum = hashlib.sha1(buf).hexdigest(),
|
||||||
|
sha256sum = hashlib.sha256(buf).hexdigest(),
|
||||||
|
sha512sum = hashlib.sha512(buf).hexdigest()
|
||||||
|
)
|
||||||
|
|
||||||
|
def serialize_certificates(g):
|
||||||
|
for common_name, path, buf, obj, server in g():
|
||||||
|
yield dict(
|
||||||
|
serial_number = "%x" % obj.serial_number,
|
||||||
|
common_name = common_name,
|
||||||
|
server = server,
|
||||||
|
# TODO: key type, key length, key exponent, key modulo
|
||||||
|
signed = obj.not_valid_before,
|
||||||
|
expires = obj.not_valid_after,
|
||||||
|
sha256sum = hashlib.sha256(buf).hexdigest()
|
||||||
|
)
|
||||||
|
|
||||||
|
if req.context.get("user").is_admin():
|
||||||
|
logger.info("Logged in authority administrator %s" % req.context.get("user"))
|
||||||
|
else:
|
||||||
|
logger.info("Logged in authority user %s" % req.context.get("user"))
|
||||||
return dict(
|
return dict(
|
||||||
user = dict(
|
user = dict(
|
||||||
name=req.context.get("user").name,
|
name=req.context.get("user").name,
|
||||||
@ -51,29 +79,31 @@ class SessionResource(object):
|
|||||||
sn=req.context.get("user").surname,
|
sn=req.context.get("user").surname,
|
||||||
mail=req.context.get("user").mail
|
mail=req.context.get("user").mail
|
||||||
),
|
),
|
||||||
request_submission_allowed = sum( # Dirty hack!
|
request_submission_allowed = config.REQUEST_SUBMISSION_ALLOWED,
|
||||||
[req.context.get("remote_addr") in j
|
|
||||||
for j in config.REQUEST_SUBNETS]),
|
|
||||||
authority = dict(
|
authority = dict(
|
||||||
|
common_name = authority.ca_cert.subject.get_attributes_for_oid(
|
||||||
|
NameOID.COMMON_NAME)[0].value,
|
||||||
outbox = dict(
|
outbox = dict(
|
||||||
server = config.OUTBOX,
|
server = config.OUTBOX,
|
||||||
name = config.OUTBOX_NAME,
|
name = config.OUTBOX_NAME,
|
||||||
mail = config.OUTBOX_MAIL
|
mail = config.OUTBOX_MAIL
|
||||||
),
|
),
|
||||||
user_certificate_enrollment=config.USER_CERTIFICATE_ENROLLMENT,
|
machine_enrollment_allowed=config.MACHINE_ENROLLMENT_ALLOWED,
|
||||||
user_mutliple_certificates=config.USER_MULTIPLE_CERTIFICATES,
|
user_enrollment_allowed=config.USER_ENROLLMENT_ALLOWED,
|
||||||
certificate = authority.certificate,
|
user_multiple_certificates=config.USER_MULTIPLE_CERTIFICATES,
|
||||||
events = config.EVENT_SOURCE_SUBSCRIBE % config.EVENT_SOURCE_TOKEN,
|
events = config.EVENT_SOURCE_SUBSCRIBE % config.EVENT_SOURCE_TOKEN,
|
||||||
requests=authority.list_requests(),
|
requests=serialize_requests(authority.list_requests),
|
||||||
signed=authority.list_signed(),
|
signed=serialize_certificates(authority.list_signed),
|
||||||
revoked=authority.list_revoked(),
|
revoked=serialize_certificates(authority.list_revoked),
|
||||||
|
users=User.objects.all(),
|
||||||
admin_users = User.objects.filter_admins(),
|
admin_users = User.objects.filter_admins(),
|
||||||
user_subnets = config.USER_SUBNETS,
|
user_subnets = config.USER_SUBNETS,
|
||||||
autosign_subnets = config.AUTOSIGN_SUBNETS,
|
autosign_subnets = config.AUTOSIGN_SUBNETS,
|
||||||
request_subnets = config.REQUEST_SUBNETS,
|
request_subnets = config.REQUEST_SUBNETS,
|
||||||
admin_subnets=config.ADMIN_SUBNETS,
|
admin_subnets=config.ADMIN_SUBNETS,
|
||||||
signature = dict(
|
signature = dict(
|
||||||
certificate_lifetime=config.CERTIFICATE_LIFETIME,
|
server_certificate_lifetime=config.SERVER_CERTIFICATE_LIFETIME,
|
||||||
|
client_certificate_lifetime=config.CLIENT_CERTIFICATE_LIFETIME,
|
||||||
revocation_list_lifetime=config.REVOCATION_LIST_LIFETIME
|
revocation_list_lifetime=config.REVOCATION_LIST_LIFETIME
|
||||||
)
|
)
|
||||||
) if req.context.get("user").is_admin() else None,
|
) if req.context.get("user").is_admin() else None,
|
||||||
@ -88,7 +118,6 @@ class StaticResource(object):
|
|||||||
self.root = os.path.realpath(root)
|
self.root = os.path.realpath(root)
|
||||||
|
|
||||||
def __call__(self, req, resp):
|
def __call__(self, req, resp):
|
||||||
|
|
||||||
path = os.path.realpath(os.path.join(self.root, req.path[1:]))
|
path = os.path.realpath(os.path.join(self.root, req.path[1:]))
|
||||||
if not path.startswith(self.root):
|
if not path.startswith(self.root):
|
||||||
raise falcon.HTTPForbidden
|
raise falcon.HTTPForbidden
|
||||||
@ -124,7 +153,7 @@ def certidude_app():
|
|||||||
from certidude import config
|
from certidude import config
|
||||||
from .bundle import BundleResource
|
from .bundle import BundleResource
|
||||||
from .revoked import RevocationListResource
|
from .revoked import RevocationListResource
|
||||||
from .signed import SignedCertificateListResource, SignedCertificateDetailResource
|
from .signed import SignedCertificateDetailResource
|
||||||
from .request import RequestListResource, RequestDetailResource
|
from .request import RequestListResource, RequestDetailResource
|
||||||
from .lease import LeaseResource, StatusFileLeaseResource
|
from .lease import LeaseResource, StatusFileLeaseResource
|
||||||
from .whois import WhoisResource
|
from .whois import WhoisResource
|
||||||
@ -138,7 +167,6 @@ def certidude_app():
|
|||||||
app.add_route("/api/certificate/", CertificateAuthorityResource())
|
app.add_route("/api/certificate/", CertificateAuthorityResource())
|
||||||
app.add_route("/api/revoked/", RevocationListResource())
|
app.add_route("/api/revoked/", RevocationListResource())
|
||||||
app.add_route("/api/signed/{cn}/", SignedCertificateDetailResource())
|
app.add_route("/api/signed/{cn}/", SignedCertificateDetailResource())
|
||||||
app.add_route("/api/signed/", SignedCertificateListResource())
|
|
||||||
app.add_route("/api/request/{cn}/", RequestDetailResource())
|
app.add_route("/api/request/{cn}/", RequestDetailResource())
|
||||||
app.add_route("/api/request/", RequestListResource())
|
app.add_route("/api/request/", RequestListResource())
|
||||||
app.add_route("/api/", SessionResource())
|
app.add_route("/api/", SessionResource())
|
||||||
@ -151,7 +179,7 @@ def certidude_app():
|
|||||||
app.add_route("/api/whois/", WhoisResource())
|
app.add_route("/api/whois/", WhoisResource())
|
||||||
|
|
||||||
# Optional user enrollment API call
|
# Optional user enrollment API call
|
||||||
if config.USER_CERTIFICATE_ENROLLMENT:
|
if config.USER_ENROLLMENT_ALLOWED:
|
||||||
app.add_route("/api/bundle/", BundleResource())
|
app.add_route("/api/bundle/", BundleResource())
|
||||||
|
|
||||||
if config.TAGGING_BACKEND == "sql":
|
if config.TAGGING_BACKEND == "sql":
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import hashlib
|
import hashlib
|
||||||
from certidude import config, authority
|
from certidude import config, authority
|
||||||
|
@ -4,91 +4,125 @@ import falcon
|
|||||||
import logging
|
import logging
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import os
|
import os
|
||||||
|
import hashlib
|
||||||
|
from base64 import b64decode
|
||||||
from certidude import config, authority, helpers, push, errors
|
from certidude import config, authority, helpers, push, errors
|
||||||
from certidude.auth import login_required, login_optional, authorize_admin
|
from certidude.auth import login_required, login_optional, authorize_admin
|
||||||
from certidude.decorators import serialize, csrf_protection
|
from certidude.decorators import serialize, csrf_protection
|
||||||
from certidude.wrappers import Request, Certificate
|
|
||||||
from certidude.firewall import whitelist_subnets, whitelist_content_types
|
from certidude.firewall import whitelist_subnets, whitelist_content_types
|
||||||
|
|
||||||
from cryptography import x509
|
from cryptography import x509
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
from cryptography.hazmat.primitives import hashes
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import padding
|
||||||
|
from cryptography.exceptions import InvalidSignature
|
||||||
|
from cryptography.x509.oid import NameOID
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
logger = logging.getLogger("api")
|
logger = logging.getLogger("api")
|
||||||
|
|
||||||
class RequestListResource(object):
|
class RequestListResource(object):
|
||||||
@serialize
|
|
||||||
@login_required
|
|
||||||
@authorize_admin
|
|
||||||
def on_get(self, req, resp):
|
|
||||||
return authority.list_requests()
|
|
||||||
|
|
||||||
|
|
||||||
@login_optional
|
@login_optional
|
||||||
@whitelist_subnets(config.REQUEST_SUBNETS)
|
@whitelist_subnets(config.REQUEST_SUBNETS)
|
||||||
@whitelist_content_types("application/pkcs10")
|
@whitelist_content_types("application/pkcs10")
|
||||||
def on_post(self, req, resp):
|
def on_post(self, req, resp):
|
||||||
"""
|
"""
|
||||||
Submit certificate signing request (CSR) in PEM format
|
Validate and parse certificate signing request
|
||||||
"""
|
"""
|
||||||
|
|
||||||
body = req.stream.read(req.content_length)
|
body = req.stream.read(req.content_length)
|
||||||
|
csr = x509.load_pem_x509_csr(body, default_backend())
|
||||||
# Normalize body, TODO: newlines
|
try:
|
||||||
if not body.endswith("\n"):
|
common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
|
||||||
body += "\n"
|
except: # ValueError?
|
||||||
|
|
||||||
csr = Request(body)
|
|
||||||
|
|
||||||
if not csr.common_name:
|
|
||||||
logger.warning(u"Rejected signing request without common name from %s",
|
logger.warning(u"Rejected signing request without common name from %s",
|
||||||
req.context.get("remote_addr"))
|
req.context.get("remote_addr"))
|
||||||
raise falcon.HTTPBadRequest(
|
raise falcon.HTTPBadRequest(
|
||||||
"Bad request",
|
"Bad request",
|
||||||
"No common name specified!")
|
"No common name specified!")
|
||||||
|
|
||||||
|
"""
|
||||||
|
Handle domain computer automatic enrollment
|
||||||
|
"""
|
||||||
machine = req.context.get("machine")
|
machine = req.context.get("machine")
|
||||||
if machine:
|
if config.MACHINE_ENROLLMENT_ALLOWED and machine:
|
||||||
if csr.common_name != machine:
|
if common_name.value != machine:
|
||||||
raise falcon.HTTPBadRequest(
|
raise falcon.HTTPBadRequest(
|
||||||
"Bad request",
|
"Bad request",
|
||||||
"Common name %s differs from Kerberos credential %s!" % (csr.common_name, machine))
|
"Common name %s differs from Kerberos credential %s!" % (common_name.value, machine))
|
||||||
|
|
||||||
# Automatic enroll with Kerberos machine cerdentials
|
# Automatic enroll with Kerberos machine cerdentials
|
||||||
resp.set_header("Content-Type", "application/x-x509-user-cert")
|
resp.set_header("Content-Type", "application/x-x509-user-cert")
|
||||||
resp.body = authority.sign(csr, overwrite=True).dump()
|
cert, resp.body = authority._sign(csr, body, overwrite=True)
|
||||||
|
logger.info(u"Automatically enrolled Kerberos authenticated machine %s from %s",
|
||||||
|
machine, req.context.get("remote_addr"))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
"""
|
||||||
# Check if this request has been already signed and return corresponding certificte if it has been signed
|
Attempt to renew certificate using currently valid key pair
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
cert = authority.get_signed(csr.common_name)
|
path, buf, cert = authority.get_signed(common_name.value)
|
||||||
except EnvironmentError:
|
except EnvironmentError:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
if cert.pubkey == csr.pubkey:
|
if cert.public_key().public_numbers() == csr.public_key().public_numbers():
|
||||||
resp.status = falcon.HTTP_SEE_OTHER
|
try:
|
||||||
resp.location = os.path.join(os.path.dirname(req.relative_uri), "signed", csr.common_name)
|
renewal_signature = b64decode(req.get_header("X-Renewal-Signature"))
|
||||||
return
|
except TypeError, ValueError: # No header supplied, redirect to signed API call
|
||||||
|
resp.status = falcon.HTTP_SEE_OTHER
|
||||||
|
resp.location = os.path.join(os.path.dirname(req.relative_uri), "signed", common_name.value)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
verifier = cert.public_key().verifier(
|
||||||
|
renewal_signature,
|
||||||
|
padding.PSS(
|
||||||
|
mgf=padding.MGF1(hashes.SHA512()),
|
||||||
|
salt_length=padding.PSS.MAX_LENGTH
|
||||||
|
),
|
||||||
|
hashes.SHA512()
|
||||||
|
)
|
||||||
|
verifier.update(buf)
|
||||||
|
verifier.update(body)
|
||||||
|
verifier.verify()
|
||||||
|
except InvalidSignature:
|
||||||
|
logger.error("Renewal failed, invalid signature supplied for %s", common_name.value)
|
||||||
|
else:
|
||||||
|
# At this point renewal signature was valid but we need to perform some extra checks
|
||||||
|
if datetime.utcnow() > cert.not_valid_after:
|
||||||
|
logger.error("Renewal failed, current certificate for %s has expired", common_name.value)
|
||||||
|
# Put on hold
|
||||||
|
elif not config.CERTIFICATE_RENEWAL_ALLOWED:
|
||||||
|
logger.error("Renewal requested for %s, but not allowed by authority settings", common_name.value)
|
||||||
|
# Put on hold
|
||||||
|
else:
|
||||||
|
resp.set_header("Content-Type", "application/x-x509-user-cert")
|
||||||
|
_, resp.body = authority._sign(csr, body, overwrite=True)
|
||||||
|
logger.info("Renewed certificate for %s", common_name.value)
|
||||||
|
return
|
||||||
|
|
||||||
# TODO: check for revoked certificates and return HTTP 410 Gone
|
|
||||||
|
|
||||||
# Process automatic signing if the IP address is whitelisted, autosigning was requested and certificate can be automatically signed
|
"""
|
||||||
if req.get_param_as_bool("autosign") and csr.is_client:
|
Process automatic signing if the IP address is whitelisted,
|
||||||
|
autosigning was requested and certificate can be automatically signed
|
||||||
|
"""
|
||||||
|
if req.get_param_as_bool("autosign") and "." not in common_name.value:
|
||||||
for subnet in config.AUTOSIGN_SUBNETS:
|
for subnet in config.AUTOSIGN_SUBNETS:
|
||||||
if req.context.get("remote_addr") in subnet:
|
if req.context.get("remote_addr") in subnet:
|
||||||
try:
|
try:
|
||||||
resp.set_header("Content-Type", "application/x-x509-user-cert")
|
resp.set_header("Content-Type", "application/x-x509-user-cert")
|
||||||
resp.body = authority.sign(csr).dump()
|
_, resp.body = authority._sign(csr, body)
|
||||||
|
logger.info("Autosigned %s as %s is whitelisted", common_name.value, req.context.get("remote_addr"))
|
||||||
return
|
return
|
||||||
except EnvironmentError: # Certificate already exists, try to save the request
|
except EnvironmentError:
|
||||||
pass
|
logger.info("Autosign for %s failed, signed certificate already exists",
|
||||||
|
common_name.value, req.context.get("remote_addr"))
|
||||||
break
|
break
|
||||||
|
|
||||||
# Attempt to save the request otherwise
|
# Attempt to save the request otherwise
|
||||||
try:
|
try:
|
||||||
csr = authority.store_request(body)
|
csr = authority.store_request(body)
|
||||||
except errors.RequestExists:
|
except errors.RequestExists:
|
||||||
# We should stil redirect client to long poll URL below
|
# We should still redirect client to long poll URL below
|
||||||
pass
|
pass
|
||||||
except errors.DuplicateCommonNameError:
|
except errors.DuplicateCommonNameError:
|
||||||
# TODO: Certificate renewal
|
# TODO: Certificate renewal
|
||||||
@ -98,12 +132,13 @@ class RequestListResource(object):
|
|||||||
"CSR with such CN already exists",
|
"CSR with such CN already exists",
|
||||||
"Will not overwrite existing certificate signing request, explicitly delete CSR and try again")
|
"Will not overwrite existing certificate signing request, explicitly delete CSR and try again")
|
||||||
else:
|
else:
|
||||||
push.publish("request-submitted", csr.common_name)
|
push.publish("request-submitted", common_name.value)
|
||||||
|
|
||||||
# Wait the certificate to be signed if waiting is requested
|
# Wait the certificate to be signed if waiting is requested
|
||||||
|
logger.info(u"Signing request %s from %s stored", common_name.value, req.context.get("remote_addr"))
|
||||||
if req.get_param("wait"):
|
if req.get_param("wait"):
|
||||||
# Redirect to nginx pub/sub
|
# Redirect to nginx pub/sub
|
||||||
url = config.LONG_POLL_SUBSCRIBE % csr.fingerprint()
|
url = config.LONG_POLL_SUBSCRIBE % hashlib.sha256(body).hexdigest()
|
||||||
click.echo("Redirecting to: %s" % url)
|
click.echo("Redirecting to: %s" % url)
|
||||||
resp.status = falcon.HTTP_SEE_OTHER
|
resp.status = falcon.HTTP_SEE_OTHER
|
||||||
resp.set_header("Location", url.encode("ascii"))
|
resp.set_header("Location", url.encode("ascii"))
|
||||||
@ -111,20 +146,17 @@ class RequestListResource(object):
|
|||||||
else:
|
else:
|
||||||
# Request was accepted, but not processed
|
# Request was accepted, but not processed
|
||||||
resp.status = falcon.HTTP_202
|
resp.status = falcon.HTTP_202
|
||||||
logger.info(u"Signing request from %s stored", req.context.get("remote_addr"))
|
|
||||||
|
|
||||||
|
|
||||||
class RequestDetailResource(object):
|
class RequestDetailResource(object):
|
||||||
@serialize
|
|
||||||
def on_get(self, req, resp, cn):
|
def on_get(self, req, resp, cn):
|
||||||
"""
|
"""
|
||||||
Fetch certificate signing request as PEM
|
Fetch certificate signing request as PEM
|
||||||
"""
|
"""
|
||||||
csr = authority.get_request(cn)
|
resp.set_header("Content-Type", "application/pkcs10")
|
||||||
|
_, resp.body, _ = authority.get_request(cn)
|
||||||
logger.debug(u"Signing request %s was downloaded by %s",
|
logger.debug(u"Signing request %s was downloaded by %s",
|
||||||
csr.common_name, req.context.get("remote_addr"))
|
cn, req.context.get("remote_addr"))
|
||||||
return csr
|
|
||||||
|
|
||||||
|
|
||||||
@csrf_protection
|
@csrf_protection
|
||||||
@login_required
|
@login_required
|
||||||
@ -133,16 +165,15 @@ class RequestDetailResource(object):
|
|||||||
"""
|
"""
|
||||||
Sign a certificate signing request
|
Sign a certificate signing request
|
||||||
"""
|
"""
|
||||||
csr = authority.get_request(cn)
|
cert, buf = authority.sign(cn, overwrite=True)
|
||||||
cert = authority.sign(csr, overwrite=True, delete=True)
|
# Mailing and long poll publishing implemented in the function above
|
||||||
os.unlink(csr.path)
|
|
||||||
resp.body = "Certificate successfully signed"
|
resp.body = "Certificate successfully signed"
|
||||||
resp.status = falcon.HTTP_201
|
resp.status = falcon.HTTP_201
|
||||||
resp.location = os.path.join(req.relative_uri, "..", "..", "signed", cn)
|
resp.location = os.path.join(req.relative_uri, "..", "..", "signed", cn)
|
||||||
logger.info(u"Signing request %s signed by %s from %s", csr.common_name,
|
logger.info(u"Signing request %s signed by %s from %s", cn,
|
||||||
req.context.get("user"), req.context.get("remote_addr"))
|
req.context.get("user"), req.context.get("remote_addr"))
|
||||||
|
|
||||||
|
|
||||||
@csrf_protection
|
@csrf_protection
|
||||||
@login_required
|
@login_required
|
||||||
@authorize_admin
|
@authorize_admin
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
|
|
||||||
|
import click
|
||||||
import falcon
|
import falcon
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from certidude import const, config
|
from certidude import const, config
|
||||||
from certidude.authority import export_crl, list_revoked
|
from certidude.authority import export_crl, list_revoked
|
||||||
from certidude.decorators import MyEncoder
|
|
||||||
from cryptography import x509
|
from cryptography import x509
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
from cryptography.hazmat.primitives.serialization import Encoding
|
from cryptography.hazmat.primitives.serialization import Encoding
|
||||||
@ -31,16 +31,13 @@ class RevocationListResource(object):
|
|||||||
resp.status = falcon.HTTP_SEE_OTHER
|
resp.status = falcon.HTTP_SEE_OTHER
|
||||||
resp.set_header("Location", url.encode("ascii"))
|
resp.set_header("Location", url.encode("ascii"))
|
||||||
logger.debug(u"Redirecting to CRL request to %s", url)
|
logger.debug(u"Redirecting to CRL request to %s", url)
|
||||||
|
resp.body = "Redirecting to %s" % url
|
||||||
else:
|
else:
|
||||||
resp.set_header("Content-Type", "application/x-pem-file")
|
resp.set_header("Content-Type", "application/x-pem-file")
|
||||||
resp.append_header(
|
resp.append_header(
|
||||||
"Content-Disposition",
|
"Content-Disposition",
|
||||||
("attachment; filename=%s-crl.pem" % const.HOSTNAME).encode("ascii"))
|
("attachment; filename=%s-crl.pem" % const.HOSTNAME).encode("ascii"))
|
||||||
resp.body = export_crl()
|
resp.body = export_crl()
|
||||||
elif req.accept.startswith("application/json"):
|
|
||||||
resp.set_header("Content-Type", "application/json")
|
|
||||||
resp.set_header("Content-Disposition", "inline")
|
|
||||||
resp.body = json.dumps(list_revoked(), cls=MyEncoder)
|
|
||||||
else:
|
else:
|
||||||
raise falcon.HTTPUnsupportedMediaType(
|
raise falcon.HTTPUnsupportedMediaType(
|
||||||
"Client did not accept application/x-pkcs7-crl or application/x-pem-file")
|
"Client did not accept application/x-pkcs7-crl or application/x-pem-file")
|
||||||
|
@ -1,38 +1,46 @@
|
|||||||
|
|
||||||
import falcon
|
import falcon
|
||||||
import logging
|
import logging
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
from certidude import authority
|
from certidude import authority
|
||||||
from certidude.auth import login_required, authorize_admin
|
from certidude.auth import login_required, authorize_admin
|
||||||
from certidude.decorators import serialize, csrf_protection
|
from certidude.decorators import csrf_protection
|
||||||
|
|
||||||
logger = logging.getLogger("api")
|
logger = logging.getLogger("api")
|
||||||
|
|
||||||
class SignedCertificateListResource(object):
|
|
||||||
@serialize
|
|
||||||
@login_required
|
|
||||||
@authorize_admin
|
|
||||||
def on_get(self, req, resp):
|
|
||||||
return {"signed":authority.list_signed()}
|
|
||||||
|
|
||||||
|
|
||||||
class SignedCertificateDetailResource(object):
|
class SignedCertificateDetailResource(object):
|
||||||
@serialize
|
|
||||||
def on_get(self, req, resp, cn):
|
def on_get(self, req, resp, cn):
|
||||||
# Compensate for NTP lag
|
|
||||||
# from time import sleep
|
preferred_type = req.client_prefers(("application/json", "application/x-pem-file"))
|
||||||
# sleep(5)
|
|
||||||
try:
|
try:
|
||||||
cert = authority.get_signed(cn)
|
path, buf, cert = authority.get_signed(cn)
|
||||||
except EnvironmentError:
|
except EnvironmentError:
|
||||||
logger.warning(u"Failed to serve non-existant certificate %s to %s",
|
logger.warning(u"Failed to serve non-existant certificate %s to %s",
|
||||||
cn, req.context.get("remote_addr"))
|
cn, req.context.get("remote_addr"))
|
||||||
resp.body = "No certificate CN=%s found" % cn
|
raise falcon.HTTPNotFound("No certificate CN=%s found" % cn)
|
||||||
raise falcon.HTTPNotFound()
|
|
||||||
else:
|
else:
|
||||||
logger.debug(u"Served certificate %s to %s",
|
if preferred_type == "application/x-pem-file":
|
||||||
cn, req.context.get("remote_addr"))
|
resp.set_header("Content-Type", "application/x-pem-file")
|
||||||
return cert
|
resp.set_header("Content-Disposition", ("attachment; filename=%s.pem" % cn))
|
||||||
|
resp.body = buf
|
||||||
|
logger.debug(u"Served certificate %s to %s as application/x-pem-file",
|
||||||
|
cn, req.context.get("remote_addr"))
|
||||||
|
elif preferred_type == "application/json":
|
||||||
|
resp.set_header("Content-Type", "application/json")
|
||||||
|
resp.set_header("Content-Disposition", ("attachment; filename=%s.json" % cn))
|
||||||
|
resp.body = json.dumps(dict(
|
||||||
|
common_name = cn,
|
||||||
|
serial_number = "%x" % cert.serial_number,
|
||||||
|
signed = cert.not_valid_before.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
|
||||||
|
expires = cert.not_valid_after.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
|
||||||
|
sha256sum = hashlib.sha256(buf).hexdigest()))
|
||||||
|
logger.debug(u"Served certificate %s to %s as application/json",
|
||||||
|
cn, req.context.get("remote_addr"))
|
||||||
|
else:
|
||||||
|
logger.debug("Client did not accept application/json or application/x-pem-file")
|
||||||
|
raise falcon.HTTPUnsupportedMediaType(
|
||||||
|
"Client did not accept application/json or application/x-pem-file")
|
||||||
|
|
||||||
@csrf_protection
|
@csrf_protection
|
||||||
@login_required
|
@login_required
|
||||||
@ -40,5 +48,5 @@ class SignedCertificateDetailResource(object):
|
|||||||
def on_delete(self, req, resp, cn):
|
def on_delete(self, req, resp, cn):
|
||||||
logger.info(u"Revoked certificate %s by %s from %s",
|
logger.info(u"Revoked certificate %s by %s from %s",
|
||||||
cn, req.context.get("user"), req.context.get("remote_addr"))
|
cn, req.context.get("user"), req.context.get("remote_addr"))
|
||||||
authority.revoke_certificate(cn)
|
authority.revoke(cn)
|
||||||
|
|
||||||
|
@ -31,6 +31,8 @@ if "kerberos" in config.AUTHENTICATION_BACKENDS:
|
|||||||
else:
|
else:
|
||||||
click.echo("Kerberos enabled, service principal is HTTP/%s" % const.FQDN)
|
click.echo("Kerberos enabled, service principal is HTTP/%s" % const.FQDN)
|
||||||
|
|
||||||
|
click.echo("Accepting requests only for realm: %s" % const.DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
def authenticate(optional=False):
|
def authenticate(optional=False):
|
||||||
def wrapper(func):
|
def wrapper(func):
|
||||||
@ -38,7 +40,7 @@ def authenticate(optional=False):
|
|||||||
# If LDAP enabled and device is not Kerberos capable fall
|
# If LDAP enabled and device is not Kerberos capable fall
|
||||||
# back to LDAP bind authentication
|
# back to LDAP bind authentication
|
||||||
if "ldap" in config.AUTHENTICATION_BACKENDS:
|
if "ldap" in config.AUTHENTICATION_BACKENDS:
|
||||||
if "Android" in req.user_agent:
|
if "Android" in req.user_agent or "iPhone" in req.user_agent:
|
||||||
return ldap_authenticate(resource, req, resp, *args, **kwargs)
|
return ldap_authenticate(resource, req, resp, *args, **kwargs)
|
||||||
|
|
||||||
# Try pre-emptive authentication
|
# Try pre-emptive authentication
|
||||||
@ -81,16 +83,20 @@ def authenticate(optional=False):
|
|||||||
raise falcon.HTTPForbidden("Forbidden",
|
raise falcon.HTTPForbidden("Forbidden",
|
||||||
"Kerberos error: %s" % (ex.args[0],))
|
"Kerberos error: %s" % (ex.args[0],))
|
||||||
|
|
||||||
user = kerberos.authGSSServerUserName(context)
|
user_principal = kerberos.authGSSServerUserName(context)
|
||||||
|
username, domain = user_principal.split("@")
|
||||||
|
if domain.lower() != const.DOMAIN:
|
||||||
|
raise falcon.HTTPForbidden("Forbidden",
|
||||||
|
"Invalid realm supplied")
|
||||||
|
|
||||||
if "$@" in user and optional:
|
if username.endswith("$") and optional:
|
||||||
# Extract machine hostname
|
# Extract machine hostname
|
||||||
# TODO: Assert LDAP group membership
|
# TODO: Assert LDAP group membership
|
||||||
req.context["machine"], _ = user.lower().split("$@", 1)
|
req.context["machine"] = username[:-1].lower()
|
||||||
req.context["user"] = None
|
req.context["user"] = None
|
||||||
else:
|
else:
|
||||||
# Attempt to look up real user
|
# Attempt to look up real user
|
||||||
req.context["user"] = User.objects.get(user)
|
req.context["user"] = User.objects.get(username)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
kerberos.authGSSServerClean(context)
|
kerberos.authGSSServerClean(context)
|
||||||
@ -143,12 +149,8 @@ def authenticate(optional=False):
|
|||||||
conn = ldap.initialize(config.LDAP_AUTHENTICATION_URI)
|
conn = ldap.initialize(config.LDAP_AUTHENTICATION_URI)
|
||||||
conn.set_option(ldap.OPT_REFERRALS, 0)
|
conn.set_option(ldap.OPT_REFERRALS, 0)
|
||||||
|
|
||||||
if "@" not in user:
|
|
||||||
user = "%s@%s" % (user, const.DOMAIN)
|
|
||||||
logger.debug("Expanded username to %s", user)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
conn.simple_bind_s(user, passwd)
|
conn.simple_bind_s("%s@%s" % (user, const.DOMAIN), passwd)
|
||||||
except ldap.STRONG_AUTH_REQUIRED:
|
except ldap.STRONG_AUTH_REQUIRED:
|
||||||
logger.critical("LDAP server demands encryption, use ldaps:// instead of ldaps://")
|
logger.critical("LDAP server demands encryption, use ldaps:// instead of ldaps://")
|
||||||
raise
|
raise
|
||||||
@ -160,8 +162,8 @@ def authenticate(optional=False):
|
|||||||
logger.critical(u"LDAP bind authentication failed for user %s from %s",
|
logger.critical(u"LDAP bind authentication failed for user %s from %s",
|
||||||
repr(user), req.context.get("remote_addr"))
|
repr(user), req.context.get("remote_addr"))
|
||||||
raise falcon.HTTPUnauthorized("Forbidden",
|
raise falcon.HTTPUnauthorized("Forbidden",
|
||||||
"Please authenticate with %s domain account or supply UPN" % const.DOMAIN,
|
"Please authenticate with %s domain account username" % const.DOMAIN,
|
||||||
("Basic",))
|
("Basic",))
|
||||||
|
|
||||||
req.context["ldap_conn"] = conn
|
req.context["ldap_conn"] = conn
|
||||||
req.context["user"] = User.objects.get(user)
|
req.context["user"] = User.objects.get(user)
|
||||||
|
@ -4,15 +4,15 @@ import os
|
|||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import requests
|
import requests
|
||||||
|
import hashlib
|
||||||
import socket
|
import socket
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
from cryptography import x509
|
from cryptography import x509
|
||||||
from cryptography.x509.oid import NameOID, ExtensionOID, AuthorityInformationAccessOID
|
from cryptography.x509.oid import NameOID, ExtensionOID, ExtendedKeyUsageOID
|
||||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||||
from cryptography.hazmat.primitives import hashes, serialization
|
from cryptography.hazmat.primitives import hashes, serialization
|
||||||
from certidude import config, push, mailer, const
|
from certidude import config, push, mailer, const
|
||||||
from certidude.wrappers import Certificate, Request
|
|
||||||
from certidude import errors
|
from certidude import errors
|
||||||
from jinja2 import Template
|
from jinja2 import Template
|
||||||
|
|
||||||
@ -23,71 +23,51 @@ RE_HOSTNAME = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z
|
|||||||
# http://pycopia.googlecode.com/svn/trunk/net/pycopia/ssl/certs.py
|
# http://pycopia.googlecode.com/svn/trunk/net/pycopia/ssl/certs.py
|
||||||
|
|
||||||
# Cache CA certificate
|
# Cache CA certificate
|
||||||
certificate = Certificate(open(config.AUTHORITY_CERTIFICATE_PATH))
|
|
||||||
|
|
||||||
def publish_certificate(func):
|
|
||||||
# TODO: Implement e-mail and nginx notifications using hooks
|
|
||||||
def wrapped(csr, *args, **kwargs):
|
|
||||||
cert = func(csr, *args, **kwargs)
|
|
||||||
assert isinstance(cert, Certificate), "notify wrapped function %s returned %s" % (func, type(cert))
|
|
||||||
|
|
||||||
recipient = None
|
|
||||||
|
|
||||||
mailer.send(
|
|
||||||
"certificate-signed.md",
|
|
||||||
to=recipient,
|
|
||||||
attachments=(cert,),
|
|
||||||
certificate=cert)
|
|
||||||
|
|
||||||
if config.LONG_POLL_PUBLISH:
|
|
||||||
url = config.LONG_POLL_PUBLISH % csr.fingerprint()
|
|
||||||
click.echo("Publishing certificate at %s ..." % url)
|
|
||||||
requests.post(url, data=cert.dump(),
|
|
||||||
headers={"User-Agent": "Certidude API", "Content-Type": "application/x-x509-user-cert"})
|
|
||||||
|
|
||||||
# For deleting request in the web view, use pubkey modulo
|
|
||||||
push.publish("request-signed", cert.common_name)
|
|
||||||
return cert
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
with open(config.AUTHORITY_CERTIFICATE_PATH) as fh:
|
||||||
|
ca_buf = fh.read()
|
||||||
|
ca_cert = x509.load_pem_x509_certificate(ca_buf, default_backend())
|
||||||
|
|
||||||
def get_request(common_name):
|
def get_request(common_name):
|
||||||
if not re.match(RE_HOSTNAME, common_name):
|
if not re.match(RE_HOSTNAME, common_name):
|
||||||
raise ValueError("Invalid common name %s" % repr(common_name))
|
raise ValueError("Invalid common name %s" % repr(common_name))
|
||||||
return Request(open(os.path.join(config.REQUESTS_DIR, common_name + ".pem")))
|
path = os.path.join(config.REQUESTS_DIR, common_name + ".pem")
|
||||||
|
with open(path) as fh:
|
||||||
|
buf = fh.read()
|
||||||
|
return path, buf, x509.load_pem_x509_csr(buf, default_backend())
|
||||||
|
|
||||||
def get_signed(common_name):
|
def get_signed(common_name):
|
||||||
if not re.match(RE_HOSTNAME, common_name):
|
if not re.match(RE_HOSTNAME, common_name):
|
||||||
raise ValueError("Invalid common name %s" % repr(common_name))
|
raise ValueError("Invalid common name %s" % repr(common_name))
|
||||||
return Certificate(open(os.path.join(config.SIGNED_DIR, common_name + ".pem")))
|
path = os.path.join(config.SIGNED_DIR, common_name + ".pem")
|
||||||
|
with open(path) as fh:
|
||||||
|
buf = fh.read()
|
||||||
def get_revoked(common_name):
|
return path, buf, x509.load_pem_x509_certificate(buf, default_backend())
|
||||||
if not re.match(RE_HOSTNAME, common_name):
|
|
||||||
raise ValueError("Invalid common name %s" % repr(common_name))
|
|
||||||
return Certificate(open(os.path.join(config.SIGNED_DIR, common_name + ".pem")))
|
|
||||||
|
|
||||||
|
def get_revoked(serial):
|
||||||
|
path = os.path.join(config.REVOKED_DIR, serial + ".pem")
|
||||||
|
with open(path) as fh:
|
||||||
|
buf = fh.read()
|
||||||
|
return path, buf, x509.load_pem_x509_certificate(buf, default_backend())
|
||||||
|
|
||||||
def store_request(buf, overwrite=False):
|
def store_request(buf, overwrite=False):
|
||||||
"""
|
"""
|
||||||
Store CSR for later processing
|
Store CSR for later processing
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not buf: return # No certificate supplied
|
if not buf:
|
||||||
|
raise ValueError("No certificate supplied") # No certificate supplied
|
||||||
|
|
||||||
csr = x509.load_pem_x509_csr(buf, backend=default_backend())
|
csr = x509.load_pem_x509_csr(buf, backend=default_backend())
|
||||||
for name in csr.subject:
|
common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
|
||||||
if name.oid == NameOID.COMMON_NAME:
|
# TODO: validate common name again
|
||||||
common_name = name.value
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise ValueError("No common name in %s" % csr.subject)
|
|
||||||
|
|
||||||
request_path = os.path.join(config.REQUESTS_DIR, common_name + ".pem")
|
if not re.match(RE_HOSTNAME, common_name.value):
|
||||||
|
|
||||||
if not re.match(RE_HOSTNAME, common_name):
|
|
||||||
raise ValueError("Invalid common name")
|
raise ValueError("Invalid common name")
|
||||||
|
|
||||||
|
request_path = os.path.join(config.REQUESTS_DIR, common_name.value + ".pem")
|
||||||
|
|
||||||
|
|
||||||
# If there is cert, check if it's the same
|
# If there is cert, check if it's the same
|
||||||
if os.path.exists(request_path):
|
if os.path.exists(request_path):
|
||||||
if open(request_path).read() == buf:
|
if open(request_path).read() == buf:
|
||||||
@ -99,9 +79,11 @@ def store_request(buf, overwrite=False):
|
|||||||
fh.write(buf)
|
fh.write(buf)
|
||||||
os.rename(request_path + ".part", request_path)
|
os.rename(request_path + ".part", request_path)
|
||||||
|
|
||||||
req = Request(open(request_path))
|
attach_csr = buf, "application/x-pem-file", common_name.value + ".csr"
|
||||||
mailer.send("request-stored.md", attachments=(req,), request=req)
|
mailer.send("request-stored.md",
|
||||||
return req
|
attachments=(attach_csr,),
|
||||||
|
common_name=common_name.value)
|
||||||
|
return csr
|
||||||
|
|
||||||
|
|
||||||
def signer_exec(cmd, *bits):
|
def signer_exec(cmd, *bits):
|
||||||
@ -118,14 +100,15 @@ def signer_exec(cmd, *bits):
|
|||||||
return buf
|
return buf
|
||||||
|
|
||||||
|
|
||||||
def revoke_certificate(common_name):
|
def revoke(common_name):
|
||||||
"""
|
"""
|
||||||
Revoke valid certificate
|
Revoke valid certificate
|
||||||
"""
|
"""
|
||||||
cert = get_signed(common_name)
|
path, buf, cert = get_signed(common_name)
|
||||||
revoked_filename = os.path.join(config.REVOKED_DIR, "%s.pem" % cert.serial_number)
|
revoked_path = os.path.join(config.REVOKED_DIR, "%x.pem" % cert.serial_number)
|
||||||
os.rename(cert.path, revoked_filename)
|
signed_path = os.path.join(config.SIGNED_DIR, "%s.pem" % common_name)
|
||||||
push.publish("certificate-revoked", cert.common_name)
|
os.rename(signed_path, revoked_path)
|
||||||
|
push.publish("certificate-revoked", common_name)
|
||||||
|
|
||||||
# Publish CRL for long polls
|
# Publish CRL for long polls
|
||||||
if config.LONG_POLL_PUBLISH:
|
if config.LONG_POLL_PUBLISH:
|
||||||
@ -134,26 +117,52 @@ def revoke_certificate(common_name):
|
|||||||
requests.post(url, data=export_crl(),
|
requests.post(url, data=export_crl(),
|
||||||
headers={"User-Agent": "Certidude API", "Content-Type": "application/x-pem-file"})
|
headers={"User-Agent": "Certidude API", "Content-Type": "application/x-pem-file"})
|
||||||
|
|
||||||
mailer.send("certificate-revoked.md", attachments=(cert,), certificate=cert)
|
attach_cert = buf, "application/x-pem-file", common_name + ".crt"
|
||||||
|
mailer.send("certificate-revoked.md",
|
||||||
|
attachments=(attach_cert,),
|
||||||
|
serial_number="%x" % cert.serial,
|
||||||
|
common_name=common_name)
|
||||||
|
|
||||||
|
def server_flags(cn):
|
||||||
|
if config.USER_ENROLLMENT_ALLOWED and not config.USER_MULTIPLE_CERTIFICATES:
|
||||||
|
# Common name set to username, used for only HTTPS client validation anyway
|
||||||
|
return False
|
||||||
|
if "@" in cn:
|
||||||
|
# username@hostname is user certificate anyway, can't be server
|
||||||
|
return False
|
||||||
|
if "." in cn:
|
||||||
|
# CN is hostname, if contains dot has to be FQDN, hence a server
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def list_requests(directory=config.REQUESTS_DIR):
|
def list_requests(directory=config.REQUESTS_DIR):
|
||||||
for filename in os.listdir(directory):
|
for filename in os.listdir(directory):
|
||||||
if filename.endswith(".pem"):
|
if filename.endswith(".pem"):
|
||||||
yield Request(open(os.path.join(directory, filename)))
|
common_name = filename[:-4]
|
||||||
|
path, buf, req = get_request(common_name)
|
||||||
|
yield common_name, path, buf, req, server_flags(common_name),
|
||||||
|
|
||||||
|
def _list_certificates(directory):
|
||||||
def list_signed(directory=config.SIGNED_DIR):
|
|
||||||
for filename in os.listdir(directory):
|
for filename in os.listdir(directory):
|
||||||
if filename.endswith(".pem"):
|
if filename.endswith(".pem"):
|
||||||
yield Certificate(open(os.path.join(directory, filename)))
|
common_name = filename[:-4]
|
||||||
|
path = os.path.join(directory, filename)
|
||||||
|
with open(path) as fh:
|
||||||
|
buf = fh.read()
|
||||||
|
cert = x509.load_pem_x509_certificate(buf, default_backend())
|
||||||
|
server = False
|
||||||
|
extension = cert.extensions.get_extension_for_oid(ExtensionOID.EXTENDED_KEY_USAGE)
|
||||||
|
for usage in extension.value:
|
||||||
|
if usage == ExtendedKeyUsageOID.SERVER_AUTH: # TODO: IKE intermediate?
|
||||||
|
server = True
|
||||||
|
yield common_name, path, buf, cert, server
|
||||||
|
|
||||||
|
def list_signed():
|
||||||
|
return _list_certificates(config.SIGNED_DIR)
|
||||||
|
|
||||||
def list_revoked(directory=config.REVOKED_DIR):
|
def list_revoked():
|
||||||
for filename in os.listdir(directory):
|
return _list_certificates(config.REVOKED_DIR)
|
||||||
if filename.endswith(".pem"):
|
|
||||||
yield Certificate(open(os.path.join(directory, filename)))
|
|
||||||
|
|
||||||
|
|
||||||
def export_crl():
|
def export_crl():
|
||||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||||
@ -178,14 +187,15 @@ def delete_request(common_name):
|
|||||||
raise ValueError("Invalid common name")
|
raise ValueError("Invalid common name")
|
||||||
|
|
||||||
path = os.path.join(config.REQUESTS_DIR, common_name + ".pem")
|
path = os.path.join(config.REQUESTS_DIR, common_name + ".pem")
|
||||||
request = Request(open(path))
|
_, buf, csr = get_request(common_name)
|
||||||
os.unlink(path)
|
os.unlink(path)
|
||||||
|
|
||||||
# Publish event at CA channel
|
# Publish event at CA channel
|
||||||
push.publish("request-deleted", request.common_name)
|
push.publish("request-deleted", common_name)
|
||||||
|
|
||||||
# Write empty certificate to long-polling URL
|
# Write empty certificate to long-polling URL
|
||||||
requests.delete(config.LONG_POLL_PUBLISH % request.fingerprint(),
|
requests.delete(
|
||||||
|
config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest(),
|
||||||
headers={"User-Agent": "Certidude API"})
|
headers={"User-Agent": "Certidude API"})
|
||||||
|
|
||||||
def generate_ovpn_bundle(common_name, owner=None):
|
def generate_ovpn_bundle(common_name, owner=None):
|
||||||
@ -198,26 +208,26 @@ def generate_ovpn_bundle(common_name, owner=None):
|
|||||||
backend=default_backend()
|
backend=default_backend()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
key_buf = key.private_bytes(
|
||||||
|
encoding=serialization.Encoding.PEM,
|
||||||
|
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||||
|
encryption_algorithm=serialization.NoEncryption()
|
||||||
|
)
|
||||||
|
|
||||||
csr = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([
|
csr = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([
|
||||||
x509.NameAttribute(k, v) for k, v in (
|
x509.NameAttribute(k, v) for k, v in (
|
||||||
(NameOID.COMMON_NAME, common_name),
|
(NameOID.COMMON_NAME, common_name),
|
||||||
) if v
|
) if v
|
||||||
]))
|
])).sign(key, hashes.SHA512(), default_backend())
|
||||||
|
|
||||||
|
buf = csr.public_bytes(serialization.Encoding.PEM)
|
||||||
|
|
||||||
# Sign CSR
|
# Sign CSR
|
||||||
cert = sign(Request(
|
cert, cert_buf = _sign(csr, buf, overwrite=True)
|
||||||
csr.sign(key, hashes.SHA512(), default_backend()).public_bytes(serialization.Encoding.PEM)), overwrite=True)
|
|
||||||
|
|
||||||
bundle = Template(open(config.OPENVPN_BUNDLE_TEMPLATE).read()).render(
|
bundle = Template(open(config.OPENVPN_PROFILE_TEMPLATE).read()).render(
|
||||||
ca = certificate.dump(),
|
ca = ca_buf, key = key_buf, cert = cert_buf, crl=export_crl(),
|
||||||
key = key.private_bytes(
|
servers = [cn for cn, path, buf, cert, server in list_signed() if server])
|
||||||
encoding=serialization.Encoding.PEM,
|
|
||||||
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
|
||||||
encryption_algorithm=serialization.NoEncryption()
|
|
||||||
),
|
|
||||||
cert = cert.dump(),
|
|
||||||
crl=export_crl(),
|
|
||||||
)
|
|
||||||
return bundle, cert
|
return bundle, cert
|
||||||
|
|
||||||
def generate_pkcs12_bundle(common_name, key_size=4096, owner=None):
|
def generate_pkcs12_bundle(common_name, key_size=4096, owner=None):
|
||||||
@ -236,11 +246,12 @@ def generate_pkcs12_bundle(common_name, key_size=4096, owner=None):
|
|||||||
|
|
||||||
csr = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([
|
csr = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([
|
||||||
x509.NameAttribute(NameOID.COMMON_NAME, common_name)
|
x509.NameAttribute(NameOID.COMMON_NAME, common_name)
|
||||||
]))
|
])).sign(key, hashes.SHA512(), default_backend())
|
||||||
|
|
||||||
|
buf = csr.public_bytes(serialization.Encoding.PEM)
|
||||||
|
|
||||||
# Sign CSR
|
# Sign CSR
|
||||||
cert = sign(Request(
|
cert, cert_buf = _sign(csr, buf, overwrite=True)
|
||||||
csr.sign(key, hashes.SHA512(), default_backend()).public_bytes(serialization.Encoding.PEM)), overwrite=True)
|
|
||||||
|
|
||||||
# Generate P12, currently supported only by PyOpenSSL
|
# Generate P12, currently supported only by PyOpenSSL
|
||||||
try:
|
try:
|
||||||
@ -256,131 +267,102 @@ def generate_pkcs12_bundle(common_name, key_size=4096, owner=None):
|
|||||||
key.private_bytes(
|
key.private_bytes(
|
||||||
encoding=serialization.Encoding.PEM,
|
encoding=serialization.Encoding.PEM,
|
||||||
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||||
encryption_algorithm=serialization.NoEncryption()
|
encryption_algorithm=serialization.NoEncryption())))
|
||||||
)
|
p12.set_certificate(
|
||||||
)
|
crypto.load_certificate(crypto.FILETYPE_PEM, cert_buf))
|
||||||
)
|
p12.set_ca_certificates([
|
||||||
p12.set_certificate( cert._obj )
|
crypto.load_certificate(crypto.FILETYPE_PEM, ca_buf)])
|
||||||
p12.set_ca_certificates([certificate._obj])
|
|
||||||
return p12.export("1234"), cert
|
return p12.export("1234"), cert
|
||||||
|
|
||||||
|
|
||||||
@publish_certificate
|
def sign(common_name, overwrite=False):
|
||||||
def sign(req, overwrite=False, delete=True):
|
|
||||||
"""
|
"""
|
||||||
Sign certificate signing request via signer process
|
Sign certificate signing request via signer process
|
||||||
"""
|
"""
|
||||||
cert_path = os.path.join(config.SIGNED_DIR, req.common_name + ".pem")
|
|
||||||
|
req_path = os.path.join(config.REQUESTS_DIR, common_name + ".pem")
|
||||||
|
with open(req_path) as fh:
|
||||||
|
csr_buf = fh.read()
|
||||||
|
csr = x509.load_pem_x509_csr(csr_buf, backend=default_backend())
|
||||||
|
common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
|
||||||
|
|
||||||
|
# Sign with function below
|
||||||
|
cert, buf = _sign(csr, csr_buf, overwrite)
|
||||||
|
|
||||||
|
os.unlink(req_path)
|
||||||
|
return cert, buf
|
||||||
|
|
||||||
|
def _sign(csr, buf, overwrite=False):
|
||||||
|
assert buf.startswith("-----BEGIN CERTIFICATE REQUEST-----\n")
|
||||||
|
assert isinstance(csr, x509.CertificateSigningRequest)
|
||||||
|
common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
|
||||||
|
cert_path = os.path.join(config.SIGNED_DIR, common_name.value + ".pem")
|
||||||
|
renew = False
|
||||||
|
|
||||||
# Move existing certificate if necessary
|
# Move existing certificate if necessary
|
||||||
if os.path.exists(cert_path):
|
if os.path.exists(cert_path):
|
||||||
old_cert = Certificate(open(cert_path))
|
with open(cert_path) as fh:
|
||||||
|
prev_buf = fh.read()
|
||||||
|
prev = x509.load_pem_x509_certificate(prev_buf, default_backend())
|
||||||
|
# TODO: assert validity here again?
|
||||||
|
renew = prev.public_key().public_numbers() == csr.public_key().public_numbers()
|
||||||
|
|
||||||
if overwrite:
|
if overwrite:
|
||||||
revoke_certificate(req.common_name)
|
if renew:
|
||||||
elif req.pubkey == old_cert.pubkey:
|
# TODO: is this the best approach?
|
||||||
return old_cert
|
signed_path = os.path.join(config.SIGNED_DIR, "%s.pem" % common_name.value)
|
||||||
|
revoked_path = os.path.join(config.REVOKED_DIR, "%x.pem" % prev.serial_number)
|
||||||
|
os.rename(signed_path, revoked_path)
|
||||||
|
else:
|
||||||
|
revoke(common_name.value)
|
||||||
else:
|
else:
|
||||||
raise EnvironmentError("Will not overwrite existing certificate")
|
raise EnvironmentError("Will not overwrite existing certificate")
|
||||||
|
|
||||||
# Sign via signer process
|
# Sign via signer process
|
||||||
cert_buf = signer_exec("sign-request", req.dump())
|
cert_buf = signer_exec("sign-request", buf)
|
||||||
|
cert = x509.load_pem_x509_certificate(cert_buf, default_backend())
|
||||||
with open(cert_path + ".part", "wb") as fh:
|
with open(cert_path + ".part", "wb") as fh:
|
||||||
fh.write(cert_buf)
|
fh.write(cert_buf)
|
||||||
os.rename(cert_path + ".part", cert_path)
|
os.rename(cert_path + ".part", cert_path)
|
||||||
|
|
||||||
return Certificate(open(cert_path))
|
# Send mail
|
||||||
|
recipient = None
|
||||||
|
|
||||||
|
if renew:
|
||||||
@publish_certificate
|
mailer.send(
|
||||||
def sign2(request, private_key, authority_certificate, overwrite=False, delete=True, lifetime=None):
|
"certificate-renewed.md",
|
||||||
"""
|
to=recipient,
|
||||||
Sign directly using private key, this is usually done by root.
|
attachments=(
|
||||||
Basic constraints and certificate lifetime are copied from config,
|
(prev_buf, "application/x-pem-file", "deprecated.crt"),
|
||||||
lifetime may be overridden on the command line,
|
(cert_buf, "application/x-pem-file", common_name.value + ".crt")
|
||||||
other extensions are copied as is.
|
),
|
||||||
"""
|
serial_number="%x" % cert.serial,
|
||||||
|
common_name=common_name.value,
|
||||||
certificate_path = os.path.join(config.SIGNED_DIR, request.common_name + ".pem")
|
certificate=cert,
|
||||||
if os.path.exists(certificate_path):
|
)
|
||||||
if overwrite:
|
else:
|
||||||
revoke_certificate(request.common_name)
|
mailer.send(
|
||||||
else:
|
"certificate-signed.md",
|
||||||
raise errors.DuplicateCommonNameError("Valid certificate with common name %s already exists" % request.common_name)
|
to=recipient,
|
||||||
|
attachments=(
|
||||||
now = datetime.utcnow()
|
(buf, "application/x-pem-file", common_name.value + ".csr"),
|
||||||
request_path = os.path.join(config.REQUESTS_DIR, request.common_name + ".pem")
|
(cert_buf, "application/x-pem-file", common_name.value + ".crt")
|
||||||
request = x509.load_pem_x509_csr(open(request_path).read(), default_backend())
|
),
|
||||||
|
serial_number="%x" % cert.serial,
|
||||||
cert = x509.CertificateBuilder(
|
common_name=common_name.value,
|
||||||
).subject_name(x509.Name([n for n in request.subject])
|
certificate=cert,
|
||||||
).serial_number(random.randint(
|
|
||||||
0x1000000000000000000000000000000000000000,
|
|
||||||
0xffffffffffffffffffffffffffffffffffffffff)
|
|
||||||
).issuer_name(authority_certificate.issuer
|
|
||||||
).public_key(request.public_key()
|
|
||||||
).not_valid_before(now - timedelta(hours=1)
|
|
||||||
).not_valid_after(now + timedelta(days=config.CERTIFICATE_LIFETIME)
|
|
||||||
).add_extension(x509.KeyUsage(
|
|
||||||
digital_signature=True,
|
|
||||||
key_encipherment=True,
|
|
||||||
content_commitment=False,
|
|
||||||
data_encipherment=False,
|
|
||||||
key_agreement=False,
|
|
||||||
key_cert_sign=False,
|
|
||||||
crl_sign=False,
|
|
||||||
encipher_only=False,
|
|
||||||
decipher_only=False), critical=True
|
|
||||||
).add_extension(
|
|
||||||
x509.SubjectKeyIdentifier.from_public_key(request.public_key()),
|
|
||||||
critical=False
|
|
||||||
).add_extension(
|
|
||||||
x509.AuthorityInformationAccess([
|
|
||||||
x509.AccessDescription(
|
|
||||||
AuthorityInformationAccessOID.CA_ISSUERS,
|
|
||||||
x509.UniformResourceIdentifier(
|
|
||||||
config.CERTIFICATE_AUTHORITY_URL)
|
|
||||||
)
|
|
||||||
]),
|
|
||||||
critical=False
|
|
||||||
).add_extension(
|
|
||||||
x509.CRLDistributionPoints([
|
|
||||||
x509.DistributionPoint(
|
|
||||||
full_name=[
|
|
||||||
x509.UniformResourceIdentifier(
|
|
||||||
config.CERTIFICATE_CRL_URL)],
|
|
||||||
relative_name=None,
|
|
||||||
crl_issuer=None,
|
|
||||||
reasons=None)
|
|
||||||
]),
|
|
||||||
critical=False
|
|
||||||
).add_extension(
|
|
||||||
x509.AuthorityKeyIdentifier.from_issuer_public_key(
|
|
||||||
authority_certificate.public_key()),
|
|
||||||
critical=False
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Append subject alternative name, extended key usage flags etc
|
|
||||||
for extension in request.extensions:
|
if config.LONG_POLL_PUBLISH:
|
||||||
if extension.oid == ExtensionOID.SUBJECT_ALTERNATIVE_NAME:
|
url = config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest()
|
||||||
click.echo("Appending subject alt name extension: %s" % extension)
|
click.echo("Publishing certificate at %s ..." % url)
|
||||||
cert = cert.add_extension(x509.SubjectAlternativeName(extension.value),
|
requests.post(url, data=cert_buf,
|
||||||
critical=extension.critical)
|
headers={"User-Agent": "Certidude API", "Content-Type": "application/x-x509-user-cert"})
|
||||||
if extension.oid == ExtensionOID.EXTENDED_KEY_USAGE:
|
|
||||||
click.echo("Appending extended key usage flags extension: %s" % extension)
|
if config.EVENT_SOURCE_PUBLISH: # TODO: handle renewal
|
||||||
cert = cert.add_extension(x509.ExtendedKeyUsage(extension.value),
|
push.publish("request-signed", common_name.value)
|
||||||
critical=extension.critical)
|
|
||||||
|
return cert, cert_buf
|
||||||
|
|
||||||
|
|
||||||
cert = cert.sign(private_key, hashes.SHA512(), default_backend())
|
|
||||||
|
|
||||||
buf = cert.public_bytes(serialization.Encoding.PEM)
|
|
||||||
with open(certificate_path + ".part", "wb") as fh:
|
|
||||||
fh.write(buf)
|
|
||||||
os.rename(certificate_path + ".part", certificate_path)
|
|
||||||
click.echo("Wrote certificate to: %s" % certificate_path)
|
|
||||||
if delete:
|
|
||||||
os.unlink(request_path)
|
|
||||||
click.echo("Deleted request: %s" % request_path)
|
|
||||||
|
|
||||||
return Certificate(open(certificate_path))
|
|
||||||
|
|
||||||
|
421
certidude/cli.py
421
certidude/cli.py
@ -25,7 +25,6 @@ from cryptography.hazmat.primitives.asymmetric import rsa
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from humanize import naturaltime
|
from humanize import naturaltime
|
||||||
from jinja2 import Environment, PackageLoader
|
from jinja2 import Environment, PackageLoader
|
||||||
from time import sleep
|
|
||||||
from setproctitle import setproctitle
|
from setproctitle import setproctitle
|
||||||
import const
|
import const
|
||||||
|
|
||||||
@ -38,22 +37,29 @@ env = Environment(loader=PackageLoader("certidude", "templates"), trim_blocks=Tr
|
|||||||
|
|
||||||
# Parse command-line argument defaults from environment
|
# Parse command-line argument defaults from environment
|
||||||
|
|
||||||
USERNAME = os.environ.get("USER")
|
|
||||||
NOW = datetime.utcnow().replace(tzinfo=None)
|
NOW = datetime.utcnow().replace(tzinfo=None)
|
||||||
FIRST_NAME = None
|
|
||||||
SURNAME = None
|
|
||||||
EMAIL = None
|
|
||||||
|
|
||||||
if USERNAME:
|
CERTIDUDE_TIMER = """
|
||||||
EMAIL = USERNAME + "@" + const.FQDN
|
[Unit]
|
||||||
|
Description=Run certidude service weekly
|
||||||
|
|
||||||
if os.getuid() >= 1000:
|
[Timer]
|
||||||
_, _, _, _, gecos, _, _ = pwd.getpwnam(USERNAME)
|
OnCalendar=weekly
|
||||||
if " " in gecos:
|
Persistent=true
|
||||||
FIRST_NAME, SURNAME = gecos.split(" ", 1)
|
Unit=certidude.service
|
||||||
else:
|
|
||||||
FIRST_NAME = gecos
|
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
"""
|
||||||
|
|
||||||
|
CERTIDUDE_SERVICE = """
|
||||||
|
[Unit]
|
||||||
|
Description=Renew certificates and update revocation lists
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=%s request
|
||||||
|
"""
|
||||||
|
|
||||||
@click.command("request", help="Run processes for requesting certificates and configuring services")
|
@click.command("request", help="Run processes for requesting certificates and configuring services")
|
||||||
@click.option("-f", "--fork", default=False, is_flag=True, help="Fork to background")
|
@click.option("-f", "--fork", default=False, is_flag=True, help="Fork to background")
|
||||||
@ -80,7 +86,24 @@ def certidude_request(fork):
|
|||||||
click.echo("Creating: %s" % run_dir)
|
click.echo("Creating: %s" % run_dir)
|
||||||
os.makedirs(run_dir)
|
os.makedirs(run_dir)
|
||||||
|
|
||||||
|
if not os.path.exists("/etc/systemd/system/certidude.timer"):
|
||||||
|
click.echo("Creating systemd timer...")
|
||||||
|
with open("/etc/systemd/system/certidude.timer", "w") as fh:
|
||||||
|
fh.write(CERTIDUDE_TIMER)
|
||||||
|
if not os.path.exists("/etc/systemd/system/certidude.service"):
|
||||||
|
click.echo("Creating systemd service...")
|
||||||
|
with open("/etc/systemd/system/certidude.service", "w") as fh:
|
||||||
|
fh.write(CERTIDUDE_SERVICE % sys.argv[0])
|
||||||
|
|
||||||
|
|
||||||
for authority in clients.sections():
|
for authority in clients.sections():
|
||||||
|
try:
|
||||||
|
endpoint_dhparam = clients.get(authority, "dhparam path")
|
||||||
|
if not os.path.exists(endpoint_dhparam):
|
||||||
|
cmd = "openssl", "dhparam", "-out", endpoint_dhparam, "2048"
|
||||||
|
subprocess.check_call(cmd)
|
||||||
|
except NoOptionError:
|
||||||
|
pass
|
||||||
try:
|
try:
|
||||||
endpoint_insecure = clients.getboolean(authority, "insecure")
|
endpoint_insecure = clients.getboolean(authority, "insecure")
|
||||||
except NoOptionError:
|
except NoOptionError:
|
||||||
@ -111,22 +134,6 @@ def certidude_request(fork):
|
|||||||
endpoint_revocations_path = "/var/lib/certidude/%s/ca_crl.pem" % authority
|
endpoint_revocations_path = "/var/lib/certidude/%s/ca_crl.pem" % authority
|
||||||
# TODO: Create directories automatically
|
# TODO: Create directories automatically
|
||||||
|
|
||||||
extended_key_usage_flags=[]
|
|
||||||
try:
|
|
||||||
endpoint_key_flags = set([j.strip() for j in clients.get(authority, "extended key usage flags").lower().split(",") if j.strip()])
|
|
||||||
except NoOptionError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
if "server auth" in endpoint_key_flags:
|
|
||||||
endpoint_key_flags -= set(["server auth"])
|
|
||||||
extended_key_usage_flags.append(ExtendedKeyUsageOID.SERVER_AUTH)
|
|
||||||
if "ike intermediate" in endpoint_key_flags:
|
|
||||||
endpoint_key_flags -= set(["ike intermediate"])
|
|
||||||
extended_key_usage_flags.append(x509.ObjectIdentifier("1.3.6.1.5.5.8.2.2"))
|
|
||||||
if endpoint_key_flags:
|
|
||||||
raise ValueError("Extended key usage flags %s not understood!" % endpoint_key_flags)
|
|
||||||
# TODO: IKE Intermediate
|
|
||||||
|
|
||||||
if clients.get(authority, "trigger") == "domain joined":
|
if clients.get(authority, "trigger") == "domain joined":
|
||||||
if not os.path.exists("/etc/krb5.keytab"):
|
if not os.path.exists("/etc/krb5.keytab"):
|
||||||
continue
|
continue
|
||||||
@ -168,8 +175,6 @@ def certidude_request(fork):
|
|||||||
endpoint_authority_path,
|
endpoint_authority_path,
|
||||||
endpoint_revocations_path,
|
endpoint_revocations_path,
|
||||||
endpoint_common_name,
|
endpoint_common_name,
|
||||||
extended_key_usage_flags,
|
|
||||||
None,
|
|
||||||
insecure=endpoint_insecure,
|
insecure=endpoint_insecure,
|
||||||
autosign=True,
|
autosign=True,
|
||||||
wait=True)
|
wait=True)
|
||||||
@ -229,6 +234,10 @@ def certidude_request(fork):
|
|||||||
|
|
||||||
# OpenVPN set up with NetworkManager
|
# OpenVPN set up with NetworkManager
|
||||||
if service_config.get(endpoint, "service") == "network-manager/openvpn":
|
if service_config.get(endpoint, "service") == "network-manager/openvpn":
|
||||||
|
nm_config_path = os.path.join("/etc/NetworkManager/system-connections", endpoint)
|
||||||
|
if os.path.exists(nm_config_path):
|
||||||
|
click.echo("Not creating %s, remove to regenerate" % nm_config_path)
|
||||||
|
continue
|
||||||
nm_config = ConfigParser()
|
nm_config = ConfigParser()
|
||||||
nm_config.add_section("connection")
|
nm_config.add_section("connection")
|
||||||
nm_config.set("connection", "id", endpoint)
|
nm_config.set("connection", "id", endpoint)
|
||||||
@ -242,6 +251,7 @@ def certidude_request(fork):
|
|||||||
nm_config.set("vpn", "tap-dev", "no")
|
nm_config.set("vpn", "tap-dev", "no")
|
||||||
nm_config.set("vpn", "remote-cert-tls", "server") # Assert TLS Server flag of X.509 certificate
|
nm_config.set("vpn", "remote-cert-tls", "server") # Assert TLS Server flag of X.509 certificate
|
||||||
nm_config.set("vpn", "remote", service_config.get(endpoint, "remote"))
|
nm_config.set("vpn", "remote", service_config.get(endpoint, "remote"))
|
||||||
|
nm_config.set("vpn", "port", "51900")
|
||||||
nm_config.set("vpn", "key", endpoint_key_path)
|
nm_config.set("vpn", "key", endpoint_key_path)
|
||||||
nm_config.set("vpn", "cert", endpoint_certificate_path)
|
nm_config.set("vpn", "cert", endpoint_certificate_path)
|
||||||
nm_config.set("vpn", "ca", endpoint_authority_path)
|
nm_config.set("vpn", "ca", endpoint_authority_path)
|
||||||
@ -255,9 +265,9 @@ def certidude_request(fork):
|
|||||||
os.umask(0o177)
|
os.umask(0o177)
|
||||||
|
|
||||||
# Write NetworkManager configuration
|
# Write NetworkManager configuration
|
||||||
with open(os.path.join("/etc/NetworkManager/system-connections", endpoint), "w") as fh:
|
with open(nm_config_path, "w") as fh:
|
||||||
nm_config.write(fh)
|
nm_config.write(fh)
|
||||||
click.echo("Created %s" % fh.name)
|
click.echo("Created %s" % nm_config_path)
|
||||||
os.system("nmcli con reload")
|
os.system("nmcli con reload")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -302,50 +312,18 @@ def certidude_request(fork):
|
|||||||
os.unlink(pid_path)
|
os.unlink(pid_path)
|
||||||
|
|
||||||
|
|
||||||
@click.command("client", help="Setup X.509 certificates for application")
|
|
||||||
@click.argument("server")
|
|
||||||
@click.option("--common-name", "-cn", default=const.HOSTNAME, help="Common name, '%s' by default" % const.HOSTNAME)
|
|
||||||
@click.option("--given-name", "-gn", default=FIRST_NAME, help="Given name of the person associted with the certificate, '%s' by default" % FIRST_NAME)
|
|
||||||
@click.option("--surname", "-sn", default=SURNAME, help="Surname of the person associted with the certificate, '%s' by default" % SURNAME)
|
|
||||||
@click.option("--key-usage", "-ku", help="Key usage attributes, none requested by default")
|
|
||||||
@click.option("--extended-key-usage", "-eku", help="Extended key usage attributes, none requested by default")
|
|
||||||
@click.option("--quiet", "-q", default=False, is_flag=True, help="Disable verbose output")
|
|
||||||
@click.option("--autosign", "-s", default=False, is_flag=True, help="Request for automatic signing if available")
|
|
||||||
@click.option("--wait", "-w", default=False, is_flag=True, help="Wait for certificate, by default return immideately")
|
|
||||||
@click.option("--key-path", "-k", default=const.HOSTNAME + ".key", help="Key path, %s.key by default" % const.HOSTNAME)
|
|
||||||
@click.option("--request-path", "-r", default=const.HOSTNAME + ".csr", help="Request path, %s.csr by default" % const.HOSTNAME)
|
|
||||||
@click.option("--certificate-path", "-c", default=const.HOSTNAME + ".crt", help="Certificate path, %s.crt by default" % const.HOSTNAME)
|
|
||||||
@click.option("--authority-path", "-a", default="ca.crt", help="Certificate authority certificate path, ca.crt by default")
|
|
||||||
@click.option("--revocations-path", "-crl", default="ca.crl", help="Certificate revocation list, ca.crl by default")
|
|
||||||
def certidude_setup_client(quiet, **kwargs):
|
|
||||||
return certidude_request_certificate(**kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
@click.command("server", help="Set up OpenVPN server")
|
@click.command("server", help="Set up OpenVPN server")
|
||||||
@click.argument("authority")
|
@click.argument("authority")
|
||||||
@click.option("--subnet", "-s", default="192.168.33.0/24", type=ip_network, help="OpenVPN subnet, 192.168.33.0/24 by default")
|
@click.option("--subnet", "-s", default="192.168.33.0/24", type=ip_network, help="OpenVPN subnet, 192.168.33.0/24 by default")
|
||||||
@click.option("--local", "-l", default="0.0.0.0", help="OpenVPN listening address, defaults to all interfaces")
|
@click.option("--local", "-l", default="0.0.0.0", help="OpenVPN listening address, defaults to all interfaces")
|
||||||
@click.option("--port", "-p", default=1194, type=click.IntRange(1,60000), help="OpenVPN listening port, 1194 by default")
|
@click.option("--port", "-p", default=51900, type=click.IntRange(1,60000), help="OpenVPN listening port, 51900 by default")
|
||||||
@click.option('--proto', "-t", default="udp", type=click.Choice(['udp', 'tcp']), help="OpenVPN transport protocol, UDP by default")
|
@click.option('--proto', "-t", default="udp", type=click.Choice(['udp', 'tcp']), help="OpenVPN transport protocol, UDP by default")
|
||||||
@click.option("--route", "-r", type=ip_network, multiple=True, help="Subnets to advertise via this connection, multiple allowed")
|
@click.option("--route", "-r", type=ip_network, multiple=True, help="Subnets to advertise via this connection, multiple allowed")
|
||||||
@click.option("--config", "-o",
|
@click.option("--config", "-o",
|
||||||
default="/etc/openvpn/site-to-client.conf",
|
default="/etc/openvpn/site-to-client.conf",
|
||||||
type=click.File(mode="w", atomic=True, lazy=True),
|
type=click.File(mode="w", atomic=True, lazy=True),
|
||||||
help="OpenVPN configuration file")
|
help="OpenVPN configuration file")
|
||||||
def certidude_setup_openvpn_server(authority, config, subnet, route, org_unit, local, proto, port):
|
def certidude_setup_openvpn_server(authority, config, subnet, route, local, proto, port):
|
||||||
|
|
||||||
# TODO: Make dirs
|
|
||||||
# TODO: Intelligent way of getting last IP address in the subnet
|
|
||||||
subnet_first = None
|
|
||||||
subnet_last = None
|
|
||||||
subnet_second = None
|
|
||||||
for addr in subnet.hosts():
|
|
||||||
if not subnet_first:
|
|
||||||
subnet_first = addr
|
|
||||||
continue
|
|
||||||
if not subnet_second:
|
|
||||||
subnet_second = addr
|
|
||||||
subnet_last = addr
|
|
||||||
|
|
||||||
# Create corresponding section in Certidude client configuration file
|
# Create corresponding section in Certidude client configuration file
|
||||||
client_config = ConfigParser()
|
client_config = ConfigParser()
|
||||||
@ -356,13 +334,12 @@ def certidude_setup_openvpn_server(authority, config, subnet, route, org_unit, l
|
|||||||
else:
|
else:
|
||||||
client_config.set(authority, "trigger", "interface up")
|
client_config.set(authority, "trigger", "interface up")
|
||||||
client_config.set(authority, "common name", const.HOSTNAME)
|
client_config.set(authority, "common name", const.HOSTNAME)
|
||||||
client_config.set(authority, "subject alternative name dns", const.FQDN)
|
|
||||||
client_config.set(authority, "extended key usage flags", "server auth")
|
|
||||||
client_config.set(authority, "request path", "/etc/openvpn/keys/%s.csr" % const.HOSTNAME)
|
client_config.set(authority, "request path", "/etc/openvpn/keys/%s.csr" % const.HOSTNAME)
|
||||||
client_config.set(authority, "key path", "/etc/openvpn/keys/%s.key" % const.HOSTNAME)
|
client_config.set(authority, "key path", "/etc/openvpn/keys/%s.key" % const.HOSTNAME)
|
||||||
client_config.set(authority, "certificate path", "/etc/openvpn/keys/%s.crt" % const.HOSTNAME)
|
client_config.set(authority, "certificate path", "/etc/openvpn/keys/%s.crt" % const.HOSTNAME)
|
||||||
client_config.set(authority, "authority path", "/etc/openvpn/keys/ca.crt")
|
client_config.set(authority, "authority path", "/etc/openvpn/keys/ca.crt")
|
||||||
client_config.set(authority, "revocations path", "/etc/openvpn/keys/ca.crl")
|
client_config.set(authority, "revocations path", "/etc/openvpn/keys/ca.crl")
|
||||||
|
client_config.set(authority, "dhparam path", "/etc/openvpn/keys/dhparam.pem")
|
||||||
with open(const.CLIENT_CONFIG_PATH + ".part", 'wb') as fh:
|
with open(const.CLIENT_CONFIG_PATH + ".part", 'wb') as fh:
|
||||||
client_config.write(fh)
|
client_config.write(fh)
|
||||||
os.rename(const.CLIENT_CONFIG_PATH + ".part", const.CLIENT_CONFIG_PATH)
|
os.rename(const.CLIENT_CONFIG_PATH + ".part", const.CLIENT_CONFIG_PATH)
|
||||||
@ -380,49 +357,38 @@ def certidude_setup_openvpn_server(authority, config, subnet, route, org_unit, l
|
|||||||
service_config.add_section(endpoint)
|
service_config.add_section(endpoint)
|
||||||
service_config.set(endpoint, "authority", authority)
|
service_config.set(endpoint, "authority", authority)
|
||||||
service_config.set(endpoint, "service", "init/openvpn")
|
service_config.set(endpoint, "service", "init/openvpn")
|
||||||
|
|
||||||
with open(const.SERVICES_CONFIG_PATH + ".part", 'wb') as fh:
|
with open(const.SERVICES_CONFIG_PATH + ".part", 'wb') as fh:
|
||||||
service_config.write(fh)
|
service_config.write(fh)
|
||||||
os.rename(const.SERVICES_CONFIG_PATH + ".part", const.SERVICES_CONFIG_PATH)
|
os.rename(const.SERVICES_CONFIG_PATH + ".part", const.SERVICES_CONFIG_PATH)
|
||||||
click.echo("Section '%s' added to %s" % (endpoint, const.SERVICES_CONFIG_PATH))
|
click.echo("Section '%s' added to %s" % (endpoint, const.SERVICES_CONFIG_PATH))
|
||||||
|
|
||||||
dhparam_path = "/etc/openvpn/keys/dhparam.pem"
|
authority_hostname = authority.split(".")[0]
|
||||||
if not os.path.exists(dhparam_path):
|
config.write("server %s %s\n" % (subnet.network_address, subnet.netmask))
|
||||||
cmd = "openssl", "dhparam", "-out", dhparam_path, "2048"
|
config.write("dev tun-%s\n" % authority_hostname)
|
||||||
subprocess.check_call(cmd)
|
|
||||||
|
|
||||||
config.write("mode server\n")
|
|
||||||
config.write("tls-server\n")
|
|
||||||
config.write("proto %s\n" % proto)
|
config.write("proto %s\n" % proto)
|
||||||
config.write("port %d\n" % port)
|
config.write("port %d\n" % port)
|
||||||
config.write("dev tap\n")
|
|
||||||
config.write("local %s\n" % local)
|
config.write("local %s\n" % local)
|
||||||
config.write("key %s\n" % client_config.get(authority, "key path"))
|
config.write("key %s\n" % client_config.get(authority, "key path"))
|
||||||
config.write("cert %s\n" % client_config.get(authority, "certificate path"))
|
config.write("cert %s\n" % client_config.get(authority, "certificate path"))
|
||||||
config.write("ca %s\n" % client_config.get(authority, "authority path"))
|
config.write("ca %s\n" % client_config.get(authority, "authority path"))
|
||||||
config.write("dh %s\n" % dhparam_path)
|
config.write("dh %s\n" % client_config.get(authority, "dhparam path"))
|
||||||
config.write("comp-lzo\n")
|
config.write("comp-lzo\n")
|
||||||
config.write("user nobody\n")
|
config.write("user nobody\n")
|
||||||
config.write("group nogroup\n")
|
config.write("group nogroup\n")
|
||||||
config.write("persist-tun\n")
|
config.write("persist-tun\n")
|
||||||
config.write("persist-key\n")
|
config.write("persist-key\n")
|
||||||
config.write("ifconfig-pool-persist /tmp/openvpn-leases.txt\n")
|
config.write("#ifconfig-pool-persist /tmp/openvpn-leases.txt\n")
|
||||||
config.write("ifconfig %s 255.255.255.0\n" % subnet_first)
|
|
||||||
config.write("server-bridge %s 255.255.255.0 %s %s\n" % (subnet_first, subnet_second, subnet_last))
|
|
||||||
config.write("#crl-verify %s\n" % client_config.get(authority, "revocations path"))
|
config.write("#crl-verify %s\n" % client_config.get(authority, "revocations path"))
|
||||||
|
|
||||||
click.echo("Generated %s" % config.name)
|
click.echo("Generated %s" % config.name)
|
||||||
click.echo("Inspect generated files and issue following to request certificate:")
|
click.echo("Inspect generated files and issue following to request certificate:")
|
||||||
click.echo()
|
click.echo()
|
||||||
click.echo(" certidude request")
|
click.echo(" certidude request")
|
||||||
click.echo()
|
|
||||||
click.echo("As OpenVPN server certificate needs specific key usage extensions please")
|
|
||||||
click.echo("use following command to sign on Certidude server instead of web interface:")
|
|
||||||
click.echo()
|
|
||||||
click.echo(" certidude sign %s" % const.HOSTNAME)
|
|
||||||
|
|
||||||
|
|
||||||
@click.command("nginx", help="Set up nginx as HTTPS server")
|
@click.command("nginx", help="Set up nginx as HTTPS server")
|
||||||
@click.argument("server")
|
@click.argument("authority")
|
||||||
@click.option("--common-name", "-cn", default=const.FQDN, help="Common name, %s by default" % const.FQDN)
|
@click.option("--common-name", "-cn", default=const.FQDN, help="Common name, %s by default" % const.FQDN)
|
||||||
@click.option("--tls-config",
|
@click.option("--tls-config",
|
||||||
default="/etc/nginx/conf.d/tls.conf",
|
default="/etc/nginx/conf.d/tls.conf",
|
||||||
@ -439,51 +405,56 @@ def certidude_setup_openvpn_server(authority, config, subnet, route, org_unit, l
|
|||||||
@click.option("--dhparam-path", "-dh", default="dhparam2048.pem", help="Diffie/Hellman parameters path, dhparam2048.pem relative to -d by default")
|
@click.option("--dhparam-path", "-dh", default="dhparam2048.pem", help="Diffie/Hellman parameters path, dhparam2048.pem relative to -d by default")
|
||||||
@click.option("--authority-path", "-ca", default="ca.crt", help="Certificate authority certificate path, ca.crt relative to -d by default")
|
@click.option("--authority-path", "-ca", default="ca.crt", help="Certificate authority certificate path, ca.crt relative to -d by default")
|
||||||
@click.option("--revocations-path", "-crl", default="ca.crl", help="Certificate revocation list, ca.crl relative to -d by default")
|
@click.option("--revocations-path", "-crl", default="ca.crl", help="Certificate revocation list, ca.crl relative to -d by default")
|
||||||
@click.option("--verify-client", "-vc", type=click.Choice(['optional', 'on', 'off']))
|
@click.option("--verify-client", "-vc", default="optional", type=click.Choice(['optional', 'on', 'off']))
|
||||||
@expand_paths()
|
@expand_paths()
|
||||||
def certidude_setup_nginx(authority, site_config, tls_config, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, revocations_path, dhparam_path, verify_client):
|
def certidude_setup_nginx(authority, site_config, tls_config, common_name, directory, key_path, request_path, certificate_path, authority_path, revocations_path, dhparam_path, verify_client):
|
||||||
# TODO: Intelligent way of getting last IP address in the subnet
|
if not os.path.exists("/etc/nginx"):
|
||||||
|
raise ValueError("nginx not installed")
|
||||||
if not os.path.exists(certificate_path):
|
if "." not in common_name:
|
||||||
click.echo("As HTTPS server certificate needs specific key usage extensions please")
|
raise ValueError("Fully qualified hostname not specified as common name, make sure hostname -f works")
|
||||||
click.echo("use following command to sign on Certidude server instead of web interface:")
|
client_config = ConfigParser()
|
||||||
click.echo()
|
if os.path.exists(const.CLIENT_CONFIG_PATH):
|
||||||
click.echo(" certidude sign %s" % common_name)
|
client_config.readfp(open(const.CLIENT_CONFIG_PATH))
|
||||||
click.echo()
|
if client_config.has_section(authority):
|
||||||
retval = certidude_request_certificate(authority, key_path, request_path,
|
click.echo("Section '%s' already exists in %s, remove to regenerate" % (authority, const.CLIENT_CONFIG_PATH))
|
||||||
certificate_path, authority_path, revocations_path, common_name, org_unit,
|
else:
|
||||||
extended_key_usage_flags = [ExtendedKeyUsageOID.SERVER_AUTH],
|
client_config.add_section(authority)
|
||||||
dns = const.FQDN, wait=True, bundle=True)
|
client_config.set(authority, "trigger", "interface up")
|
||||||
|
client_config.set(authority, "common name", common_name)
|
||||||
if not os.path.exists(dhparam_path):
|
client_config.set(authority, "request path", request_path)
|
||||||
cmd = "openssl", "dhparam", "-out", dhparam_path, "2048"
|
client_config.set(authority, "key path", key_path)
|
||||||
subprocess.check_call(cmd)
|
client_config.set(authority, "certificate path", certificate_path)
|
||||||
|
client_config.set(authority, "authority path", authority_path)
|
||||||
if retval:
|
client_config.set(authority, "dhparam path", dhparam_path)
|
||||||
return retval
|
client_config.set(authority, "revocations path", revocations_path)
|
||||||
|
with open(const.CLIENT_CONFIG_PATH + ".part", 'wb') as fh:
|
||||||
|
client_config.write(fh)
|
||||||
|
os.rename(const.CLIENT_CONFIG_PATH + ".part", const.CLIENT_CONFIG_PATH)
|
||||||
|
click.echo("Section '%s' added to %s" % (authority, const.CLIENT_CONFIG_PATH))
|
||||||
|
|
||||||
context = globals() # Grab const.BLAH
|
context = globals() # Grab const.BLAH
|
||||||
context.update(locals())
|
context.update(locals())
|
||||||
|
|
||||||
if os.path.exists(site_client_config.name):
|
if os.path.exists(site_config.name):
|
||||||
click.echo("Configuration file %s already exists, not overwriting" % site_client_config.name)
|
click.echo("Configuration file %s already exists, not overwriting" % site_config.name)
|
||||||
else:
|
else:
|
||||||
site_client_config.write(env.get_template("nginx-https-site.conf").render(context))
|
site_config.write(env.get_template("nginx-https-site.conf").render(context))
|
||||||
click.echo("Generated %s" % site_client_config.name)
|
click.echo("Generated %s" % site_config.name)
|
||||||
|
|
||||||
if os.path.exists(tls_client_config.name):
|
if os.path.exists(tls_config.name):
|
||||||
click.echo("Configuration file %s already exists, not overwriting" % tls_client_config.name)
|
click.echo("Configuration file %s already exists, not overwriting" % tls_config.name)
|
||||||
else:
|
else:
|
||||||
tls_client_config.write(env.get_template("nginx-tls.conf").render(context))
|
tls_config.write(env.get_template("nginx-tls.conf").render(context))
|
||||||
click.echo("Generated %s" % tls_client_config.name)
|
click.echo("Generated %s" % tls_config.name)
|
||||||
|
|
||||||
|
|
||||||
click.echo()
|
click.echo()
|
||||||
click.echo("Inspect configuration files, enable it and start nginx service:")
|
click.echo("Inspect configuration files, enable it and start nginx service:")
|
||||||
click.echo()
|
click.echo()
|
||||||
click.echo(" ln -s %s /etc/nginx/sites-enabled/%s" % (
|
click.echo(" ln -s %s /etc/nginx/sites-enabled/%s" % (
|
||||||
os.path.relpath(site_client_config.name, "/etc/nginx/sites-enabled"),
|
os.path.relpath(site_config.name, "/etc/nginx/sites-enabled"),
|
||||||
os.path.basename(site_client_config.name)))
|
os.path.basename(site_config.name)))
|
||||||
click.secho(" service nginx restart", bold=True)
|
click.echo(" service nginx restart")
|
||||||
click.echo()
|
click.echo()
|
||||||
|
|
||||||
|
|
||||||
@ -495,7 +466,7 @@ def certidude_setup_nginx(authority, site_config, tls_config, common_name, org_u
|
|||||||
default="/etc/openvpn/client-to-site.conf",
|
default="/etc/openvpn/client-to-site.conf",
|
||||||
type=click.File(mode="w", atomic=True, lazy=True),
|
type=click.File(mode="w", atomic=True, lazy=True),
|
||||||
help="OpenVPN configuration file")
|
help="OpenVPN configuration file")
|
||||||
def certidude_setup_openvpn_client(authority, remote, config, org_unit, proto):
|
def certidude_setup_openvpn_client(authority, remote, config, proto):
|
||||||
|
|
||||||
# Create corresponding section in Certidude client configuration file
|
# Create corresponding section in Certidude client configuration file
|
||||||
client_config = ConfigParser()
|
client_config = ConfigParser()
|
||||||
@ -553,15 +524,10 @@ def certidude_setup_openvpn_client(authority, remote, config, org_unit, proto):
|
|||||||
click.echo("Inspect generated files and issue following to request certificate:")
|
click.echo("Inspect generated files and issue following to request certificate:")
|
||||||
click.echo()
|
click.echo()
|
||||||
click.echo(" certidude request")
|
click.echo(" certidude request")
|
||||||
click.echo()
|
|
||||||
click.echo("As OpenVPN server certificate needs specific key usage extensions please")
|
|
||||||
click.echo("use following command to sign on Certidude server instead of web interface:")
|
|
||||||
click.echo()
|
|
||||||
click.echo(" certidude sign %s" % const.HOSTNAME)
|
|
||||||
|
|
||||||
|
|
||||||
@click.command("server", help="Set up strongSwan server")
|
@click.command("server", help="Set up strongSwan server")
|
||||||
@click.argument("server")
|
@click.argument("authority")
|
||||||
@click.option("--subnet", "-sn", default=u"192.168.33.0/24", type=ip_network, help="IPsec virtual subnet, 192.168.33.0/24 by default")
|
@click.option("--subnet", "-sn", default=u"192.168.33.0/24", type=ip_network, help="IPsec virtual subnet, 192.168.33.0/24 by default")
|
||||||
@click.option("--local", "-l", type=ip_address, help="IP address associated with the certificate, none by default")
|
@click.option("--local", "-l", type=ip_address, help="IP address associated with the certificate, none by default")
|
||||||
@click.option("--route", "-r", type=ip_network, multiple=True, help="Subnets to advertise via this connection, multiple allowed")
|
@click.option("--route", "-r", type=ip_network, multiple=True, help="Subnets to advertise via this connection, multiple allowed")
|
||||||
@ -580,8 +546,6 @@ def certidude_setup_strongswan_server(authority, config, secrets, subnet, route,
|
|||||||
else:
|
else:
|
||||||
client_config.set(authority, "trigger", "interface up")
|
client_config.set(authority, "trigger", "interface up")
|
||||||
client_config.set(authority, "common name", const.FQDN)
|
client_config.set(authority, "common name", const.FQDN)
|
||||||
client_config.set(authority, "subject alternative name dns", const.FQDN)
|
|
||||||
client_config.set(authority, "extended key usage flags", "server auth, ike intermediate")
|
|
||||||
client_config.set(authority, "request path", "/etc/ipsec.d/reqs/%s.pem" % const.HOSTNAME)
|
client_config.set(authority, "request path", "/etc/ipsec.d/reqs/%s.pem" % const.HOSTNAME)
|
||||||
client_config.set(authority, "key path", "/etc/ipsec.d/private/%s.pem" % const.HOSTNAME)
|
client_config.set(authority, "key path", "/etc/ipsec.d/private/%s.pem" % const.HOSTNAME)
|
||||||
client_config.set(authority, "certificate path", "/etc/ipsec.d/certs/%s.pem" % const.HOSTNAME)
|
client_config.set(authority, "certificate path", "/etc/ipsec.d/certs/%s.pem" % const.HOSTNAME)
|
||||||
@ -617,9 +581,9 @@ def certidude_setup_strongswan_server(authority, config, secrets, subnet, route,
|
|||||||
|
|
||||||
|
|
||||||
@click.command("client", help="Set up strongSwan client")
|
@click.command("client", help="Set up strongSwan client")
|
||||||
@click.argument("server")
|
@click.argument("authority")
|
||||||
@click.argument("remote")
|
@click.argument("remote")
|
||||||
def certidude_setup_strongswan_client(authority, config, org_unit, remote, dpdaction):
|
def certidude_setup_strongswan_client(authority, config, remote, dpdaction):
|
||||||
# Create corresponding section in /etc/certidude/client.conf
|
# Create corresponding section in /etc/certidude/client.conf
|
||||||
client_config = ConfigParser()
|
client_config = ConfigParser()
|
||||||
if os.path.exists(const.CLIENT_CONFIG_PATH):
|
if os.path.exists(const.CLIENT_CONFIG_PATH):
|
||||||
@ -664,9 +628,9 @@ def certidude_setup_strongswan_client(authority, config, org_unit, remote, dpdac
|
|||||||
|
|
||||||
|
|
||||||
@click.command("networkmanager", help="Set up strongSwan client via NetworkManager")
|
@click.command("networkmanager", help="Set up strongSwan client via NetworkManager")
|
||||||
@click.argument("server") # Certidude server
|
@click.argument("authority") # Certidude server
|
||||||
@click.argument("remote") # StrongSwan gateway
|
@click.argument("remote") # StrongSwan gateway
|
||||||
def certidude_setup_strongswan_networkmanager(server,remote, org_unit):
|
def certidude_setup_strongswan_networkmanager(authority, remote):
|
||||||
endpoint = "IPSec to %s" % remote
|
endpoint = "IPSec to %s" % remote
|
||||||
|
|
||||||
# Create corresponding section in /etc/certidude/client.conf
|
# Create corresponding section in /etc/certidude/client.conf
|
||||||
@ -679,7 +643,6 @@ def certidude_setup_strongswan_networkmanager(server,remote, org_unit):
|
|||||||
client_config.add_section(authority)
|
client_config.add_section(authority)
|
||||||
client_config.set(authority, "trigger", "interface up")
|
client_config.set(authority, "trigger", "interface up")
|
||||||
client_config.set(authority, "common name", const.HOSTNAME)
|
client_config.set(authority, "common name", const.HOSTNAME)
|
||||||
client_config.set(authority, "org unit", org_unit)
|
|
||||||
client_config.set(authority, "request path", "/etc/ipsec.d/reqs/%s.pem" % const.HOSTNAME)
|
client_config.set(authority, "request path", "/etc/ipsec.d/reqs/%s.pem" % const.HOSTNAME)
|
||||||
client_config.set(authority, "key path", "/etc/ipsec.d/private/%s.pem" % const.HOSTNAME)
|
client_config.set(authority, "key path", "/etc/ipsec.d/private/%s.pem" % const.HOSTNAME)
|
||||||
client_config.set(authority, "certificate path", "/etc/ipsec.d/certs/%s.pem" % const.HOSTNAME)
|
client_config.set(authority, "certificate path", "/etc/ipsec.d/certs/%s.pem" % const.HOSTNAME)
|
||||||
@ -708,10 +671,10 @@ def certidude_setup_strongswan_networkmanager(server,remote, org_unit):
|
|||||||
|
|
||||||
|
|
||||||
@click.command("networkmanager", help="Set up OpenVPN client via NetworkManager")
|
@click.command("networkmanager", help="Set up OpenVPN client via NetworkManager")
|
||||||
@click.argument("server") # Certidude server
|
@click.argument("authority")
|
||||||
@click.argument("remote") # OpenVPN gateway
|
@click.argument("remote") # OpenVPN gateway
|
||||||
@click.option("--common-name", "-cn", default=const.HOSTNAME, help="Common name, %s by default" % const.HOSTNAME)
|
@click.option("--common-name", "-cn", default=const.HOSTNAME, help="Common name, %s by default" % const.HOSTNAME)
|
||||||
def certidude_setup_openvpn_networkmanager(authority, org_unit, remote):
|
def certidude_setup_openvpn_networkmanager(authority, remote):
|
||||||
# Create corresponding section in /etc/certidude/client.conf
|
# Create corresponding section in /etc/certidude/client.conf
|
||||||
client_config = ConfigParser()
|
client_config = ConfigParser()
|
||||||
if os.path.exists(const.CLIENT_CONFIG_PATH):
|
if os.path.exists(const.CLIENT_CONFIG_PATH):
|
||||||
@ -750,29 +713,24 @@ def certidude_setup_openvpn_networkmanager(authority, org_unit, remote):
|
|||||||
|
|
||||||
@click.command("authority", help="Set up Certificate Authority in a directory")
|
@click.command("authority", help="Set up Certificate Authority in a directory")
|
||||||
@click.option("--username", default="certidude", help="Service user account, created if necessary, 'certidude' by default")
|
@click.option("--username", default="certidude", help="Service user account, created if necessary, 'certidude' by default")
|
||||||
@click.option("--static-path", default=os.path.join(os.path.dirname(__file__), "static"), help="Path to Certidude's static JS/CSS/etc")
|
|
||||||
@click.option("--kerberos-keytab", default="/etc/certidude/server.keytab", help="Kerberos keytab for using 'kerberos' authentication backend, /etc/certidude/server.keytab by default")
|
@click.option("--kerberos-keytab", default="/etc/certidude/server.keytab", help="Kerberos keytab for using 'kerberos' authentication backend, /etc/certidude/server.keytab by default")
|
||||||
@click.option("--nginx-config", "-n",
|
@click.option("--nginx-config", "-n",
|
||||||
default="/etc/nginx/sites-available/certidude.conf",
|
default="/etc/nginx/sites-available/certidude.conf",
|
||||||
type=click.File(mode="w", atomic=True, lazy=True),
|
type=click.File(mode="w", atomic=True, lazy=True),
|
||||||
help="nginx site config for serving Certidude, /etc/nginx/sites-available/certidude by default")
|
help="nginx site config for serving Certidude, /etc/nginx/sites-available/certidude by default")
|
||||||
@click.option("--parent", "-p", help="Parent CA, none by default")
|
|
||||||
@click.option("--common-name", "-cn", default=const.FQDN, help="Common name, fully qualified hostname by default")
|
@click.option("--common-name", "-cn", default=const.FQDN, help="Common name, fully qualified hostname by default")
|
||||||
@click.option("--country", "-c", default=None, help="Country, none by default")
|
@click.option("--country", "-c", default=None, help="Country, none by default")
|
||||||
@click.option("--state", "-s", default=None, help="State or country, none by default")
|
@click.option("--state", "-s", default=None, help="State or country, none by default")
|
||||||
@click.option("--locality", "-l", default=None, help="City or locality, none by default")
|
@click.option("--locality", "-l", default=None, help="City or locality, none by default")
|
||||||
@click.option("--authority-lifetime", default=20*365, help="Authority certificate lifetime in days, 7300 days (20 years) by default")
|
@click.option("--authority-lifetime", default=20*365, help="Authority certificate lifetime in days, 20 years by default")
|
||||||
@click.option("--certificate-lifetime", default=5*365, help="Certificate lifetime in days, 1825 days (5 years) by default")
|
|
||||||
@click.option("--revocation-list-lifetime", default=20*60, help="Revocation list lifetime in days, 1200 seconds (20 minutes) by default")
|
|
||||||
@click.option("--organization", "-o", default=None, help="Company or organization name")
|
@click.option("--organization", "-o", default=None, help="Company or organization name")
|
||||||
@click.option("--organizational-unit", "-ou", default=None)
|
@click.option("--organizational-unit", "-ou", default=None)
|
||||||
@click.option("--revoked-url", default=None, help="CRL distribution URL")
|
|
||||||
@click.option("--certificate-url", default=None, help="Authority certificate URL")
|
|
||||||
@click.option("--push-server", default="http://" + const.FQDN, help="Push server, by default http://%s" % const.FQDN)
|
@click.option("--push-server", default="http://" + const.FQDN, help="Push server, by default http://%s" % const.FQDN)
|
||||||
@click.option("--directory", help="Directory for authority files")
|
@click.option("--directory", help="Directory for authority files")
|
||||||
@click.option("--server-flags", is_flag=True, help="Add TLS Server and IKE Intermediate extended key usage flags")
|
@click.option("--server-flags", is_flag=True, help="Add TLS Server and IKE Intermediate extended key usage flags")
|
||||||
@click.option("--outbox", default="smtp://smtp.%s" % const.DOMAIN, help="SMTP server, smtp://smtp.%s by default" % const.DOMAIN)
|
@click.option("--outbox", default="smtp://smtp.%s" % const.DOMAIN, help="SMTP server, smtp://smtp.%s by default" % const.DOMAIN)
|
||||||
def certidude_setup_authority(username, static_path, kerberos_keytab, nginx_config, parent, country, state, locality, organization, organizational_unit, common_name, directory, certificate_lifetime, authority_lifetime, revocation_list_lifetime, revoked_url, certificate_url, push_server, outbox, server_flags):
|
def certidude_setup_authority(username, kerberos_keytab, nginx_config, country, state, locality, organization, organizational_unit, common_name, directory, authority_lifetime, push_server, outbox, server_flags):
|
||||||
|
openvpn_profile_template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates", "openvpn-client.conf")
|
||||||
|
|
||||||
if not directory:
|
if not directory:
|
||||||
if os.getuid():
|
if os.getuid():
|
||||||
@ -781,18 +739,13 @@ def certidude_setup_authority(username, static_path, kerberos_keytab, nginx_conf
|
|||||||
directory = os.path.join("/var/lib/certidude", const.FQDN)
|
directory = os.path.join("/var/lib/certidude", const.FQDN)
|
||||||
|
|
||||||
click.echo("Using fully qualified hostname: %s" % common_name)
|
click.echo("Using fully qualified hostname: %s" % common_name)
|
||||||
|
certificate_url = "http://%s/api/certificate/" % common_name
|
||||||
|
revoked_url = "http://%s/api/revoked/" % common_name
|
||||||
|
|
||||||
# Expand variables
|
# Expand variables
|
||||||
if not revoked_url:
|
|
||||||
revoked_url = "http://%s/api/revoked/" % common_name
|
|
||||||
if not certificate_url:
|
|
||||||
certificate_url = "http://%s/api/certificate/" % common_name
|
|
||||||
ca_key = os.path.join(directory, "ca_key.pem")
|
ca_key = os.path.join(directory, "ca_key.pem")
|
||||||
ca_crt = os.path.join(directory, "ca_crt.pem")
|
ca_crt = os.path.join(directory, "ca_crt.pem")
|
||||||
|
|
||||||
if not static_path.endswith("/"):
|
|
||||||
static_path += "/"
|
|
||||||
|
|
||||||
if os.getuid() == 0:
|
if os.getuid() == 0:
|
||||||
try:
|
try:
|
||||||
pwd.getpwnam("certidude")
|
pwd.getpwnam("certidude")
|
||||||
@ -833,6 +786,7 @@ def certidude_setup_authority(username, static_path, kerberos_keytab, nginx_conf
|
|||||||
working_directory = os.path.realpath(os.path.dirname(__file__))
|
working_directory = os.path.realpath(os.path.dirname(__file__))
|
||||||
certidude_path = sys.argv[0]
|
certidude_path = sys.argv[0]
|
||||||
|
|
||||||
|
# Push server config generation
|
||||||
if not os.path.exists("/etc/nginx"):
|
if not os.path.exists("/etc/nginx"):
|
||||||
click.echo("Directory /etc/nginx does not exist, hence not creating nginx configuration")
|
click.echo("Directory /etc/nginx does not exist, hence not creating nginx configuration")
|
||||||
listen = "0.0.0.0"
|
listen = "0.0.0.0"
|
||||||
@ -924,7 +878,6 @@ def certidude_setup_authority(username, static_path, kerberos_keytab, nginx_conf
|
|||||||
).add_extension(
|
).add_extension(
|
||||||
x509.AuthorityKeyIdentifier.from_issuer_public_key(key.public_key()),
|
x509.AuthorityKeyIdentifier.from_issuer_public_key(key.public_key()),
|
||||||
critical=False
|
critical=False
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if server_flags:
|
if server_flags:
|
||||||
@ -1002,121 +955,100 @@ def certidude_list(verbose, show_key_type, show_extensions, show_path, show_sign
|
|||||||
from certidude import authority
|
from certidude import authority
|
||||||
from pycountry import countries
|
from pycountry import countries
|
||||||
|
|
||||||
def dump_common(j):
|
def dump_common(common_name, path, cert):
|
||||||
|
click.echo("certidude revoke %s" % common_name)
|
||||||
person = [j for j in (j.given_name, j.surname) if j]
|
with open(path, "rb") as fh:
|
||||||
if person:
|
buf = fh.read()
|
||||||
click.echo("Associated person: %s" % " ".join(person) + (" <%s>" % j.email_address if j.email_address else ""))
|
click.echo("md5sum: %s" % hashlib.md5(buf).hexdigest())
|
||||||
elif j.email_address:
|
click.echo("sha1sum: %s" % hashlib.sha1(buf).hexdigest())
|
||||||
click.echo("Associated e-mail: " + j.email_address)
|
click.echo("sha256sum: %s" % hashlib.sha256(buf).hexdigest())
|
||||||
|
click.echo()
|
||||||
bits = [j for j in (
|
for ext in cert.extensions:
|
||||||
countries.get(alpha2=j.country_code.upper()).name if
|
print " -", ext.value
|
||||||
j.country_code else "",
|
click.echo()
|
||||||
j.state_or_county,
|
|
||||||
j.city,
|
|
||||||
j.organization,
|
|
||||||
j.organizational_unit) if j]
|
|
||||||
if bits:
|
|
||||||
click.echo("Organization: %s" % ", ".join(bits))
|
|
||||||
|
|
||||||
if show_key_type:
|
|
||||||
click.echo("Key type: %s-bit %s" % (j.key_length, j.key_type))
|
|
||||||
|
|
||||||
if show_extensions:
|
|
||||||
for key, value, data in j.extensions:
|
|
||||||
click.echo(("Extension " + key + ":").ljust(50) + " " + value)
|
|
||||||
else:
|
|
||||||
if j.key_usage:
|
|
||||||
click.echo("Key usage: " + j.key_usage)
|
|
||||||
if j.fqdn:
|
|
||||||
click.echo("Associated hostname: " + j.fqdn)
|
|
||||||
|
|
||||||
|
|
||||||
if not hide_requests:
|
if not hide_requests:
|
||||||
for j in authority.list_requests():
|
for common_name, path, buf, csr, server in authority.list_requests():
|
||||||
|
created = 0
|
||||||
if not verbose:
|
if not verbose:
|
||||||
click.echo("s " + j.path + " " + j.identity)
|
click.echo("s " + path)
|
||||||
continue
|
continue
|
||||||
click.echo(click.style(j.common_name, fg="blue"))
|
click.echo(click.style(common_name, fg="blue"))
|
||||||
click.echo("=" * len(j.common_name))
|
click.echo("=" * len(common_name))
|
||||||
click.echo("State: ? " + click.style("submitted", fg="yellow") + " " + naturaltime(j.created) + click.style(", %s" %j.created, fg="white"))
|
click.echo("State: ? " + click.style("submitted", fg="yellow") + " " + naturaltime(created) + click.style(", %s" %created, fg="white"))
|
||||||
|
click.echo("openssl req -in %s -text -noout" % path)
|
||||||
|
dump_common(common_name, path, cert)
|
||||||
|
|
||||||
dump_common(j)
|
|
||||||
|
|
||||||
# Calculate checksums for cross-checking
|
|
||||||
import hashlib
|
|
||||||
md5sum = hashlib.md5()
|
|
||||||
sha1sum = hashlib.sha1()
|
|
||||||
sha256sum = hashlib.sha256()
|
|
||||||
with open(j.path, "rb") as fh:
|
|
||||||
buf = fh.read()
|
|
||||||
md5sum.update(buf)
|
|
||||||
sha1sum.update(buf)
|
|
||||||
sha256sum.update(buf)
|
|
||||||
click.echo("MD5 checksum: %s" % md5sum.hexdigest())
|
|
||||||
click.echo("SHA-1 checksum: %s" % sha1sum.hexdigest())
|
|
||||||
click.echo("SHA-256 checksum: %s" % sha256sum.hexdigest())
|
|
||||||
|
|
||||||
if show_path:
|
|
||||||
click.echo("Details: openssl req -in %s -text -noout" % j.path)
|
|
||||||
click.echo("Sign: certidude sign %s" % j.path)
|
|
||||||
click.echo()
|
|
||||||
|
|
||||||
if show_signed:
|
if show_signed:
|
||||||
for j in authority.list_signed():
|
for common_name, path, buf, cert, server in authority.list_signed():
|
||||||
if not verbose:
|
if not verbose:
|
||||||
if j.signed < NOW and j.expires > NOW:
|
if cert.not_valid_before < NOW and cert.not_valid_after > NOW:
|
||||||
click.echo("v " + j.path + " " + j.identity)
|
click.echo("v " + path)
|
||||||
elif NOW > j.expires:
|
elif NOW > cert.not_valid_after:
|
||||||
click.echo("e " + j.path + " " + j.identity)
|
click.echo("e " + path)
|
||||||
else:
|
else:
|
||||||
click.echo("y " + j.path + " " + j.identity)
|
click.echo("y " + path)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
click.echo(click.style(j.common_name, fg="blue") + " " + click.style(j.serial_number_hex, fg="white"))
|
click.echo(click.style(common_name, fg="blue") + " " + click.style("%x" % cert.serial_number, fg="white"))
|
||||||
click.echo("="*(len(j.common_name)+60))
|
click.echo("="*(len(common_name)+60))
|
||||||
|
expires = 0 # TODO
|
||||||
if j.signed < NOW and j.expires > NOW:
|
if cert.not_valid_before < NOW and cert.not_valid_after > NOW:
|
||||||
click.echo("Status: \u2713 " + click.style("valid", fg="green") + " " + naturaltime(j.expires) + click.style(", %s" %j.expires, fg="white"))
|
click.echo("Status: " + click.style("valid", fg="green") + " until " + naturaltime(cert.not_valid_after) + click.style(", %s" % cert.not_valid_after, fg="white"))
|
||||||
elif NOW > j.expires:
|
elif NOW > cert.not_valid_after:
|
||||||
click.echo("Status: \u2717 " + click.style("expired", fg="red") + " " + naturaltime(j.expires) + click.style(", %s" %j.expires, fg="white"))
|
click.echo("Status: " + click.style("expired", fg="red") + " " + naturaltime(expires) + click.style(", %s" %expires, fg="white"))
|
||||||
else:
|
else:
|
||||||
click.echo("Status: \u2717 " + click.style("not valid yet", fg="red") + click.style(", %s" %j.expires, fg="white"))
|
click.echo("Status: " + click.style("not valid yet", fg="red") + click.style(", %s" %expires, fg="white"))
|
||||||
dump_common(j)
|
|
||||||
|
|
||||||
if show_path:
|
|
||||||
click.echo("Details: openssl x509 -in %s -text -noout" % j.path)
|
|
||||||
click.echo("Revoke: certidude revoke %s" % j.path)
|
|
||||||
click.echo()
|
click.echo()
|
||||||
|
click.echo("openssl x509 -in %s -text -noout" % path)
|
||||||
|
dump_common(common_name, path, cert)
|
||||||
|
|
||||||
if show_revoked:
|
if show_revoked:
|
||||||
for j in authority.list_revoked():
|
for common_name, path, buf, cert, server in authority.list_revoked():
|
||||||
if not verbose:
|
if not verbose:
|
||||||
click.echo("r " + j.path + " " + j.identity)
|
click.echo("r " + path)
|
||||||
continue
|
continue
|
||||||
click.echo(click.style(j.common_name, fg="blue") + " " + click.style(j.serial_number_hex, fg="white"))
|
|
||||||
click.echo("="*(len(j.common_name)+60))
|
click.echo(click.style(common_name, fg="blue") + " " + click.style("%x" % cert.serial, fg="white"))
|
||||||
click.echo("Status: \u2717 " + click.style("revoked", fg="red") + " %s%s" % (naturaltime(NOW-j.changed), click.style(", %s" % j.changed, fg="white")))
|
click.echo("="*(len(common_name)+60))
|
||||||
dump_common(j)
|
|
||||||
if show_path:
|
_, _, _, _, _, _, _, _, mtime, _ = os.stat(path)
|
||||||
click.echo("Details: openssl x509 -in %s -text -noout" % j.path)
|
changed = datetime.fromtimestamp(mtime)
|
||||||
click.echo()
|
click.echo("Status: " + click.style("revoked", fg="red") + " %s%s" % (naturaltime(NOW-changed), click.style(", %s" % changed, fg="white")))
|
||||||
|
click.echo("openssl x509 -in %s -text -noout" % path)
|
||||||
|
dump_common(common_name, path, cert)
|
||||||
|
|
||||||
click.echo()
|
click.echo()
|
||||||
|
|
||||||
|
|
||||||
@click.command("sign", help="Sign certificates")
|
@click.command("sign", help="Sign certificate")
|
||||||
@click.argument("common_name")
|
@click.argument("common_name")
|
||||||
@click.option("--overwrite", "-o", default=False, is_flag=True, help="Revoke valid certificate with same CN")
|
@click.option("--overwrite", "-o", default=False, is_flag=True, help="Revoke valid certificate with same CN")
|
||||||
@click.option("--lifetime", "-l", help="Lifetime")
|
def certidude_sign(common_name, overwrite):
|
||||||
def certidude_sign(common_name, overwrite, lifetime):
|
from certidude import authority
|
||||||
from certidude import authority, config
|
cert = authority.sign(common_name, overwrite)
|
||||||
request = authority.get_request(common_name)
|
|
||||||
cert = authority.sign(request)
|
|
||||||
|
|
||||||
|
|
||||||
|
@click.command("revoke", help="Revoke certificate")
|
||||||
|
@click.argument("common_name")
|
||||||
|
def certidude_revoke(common_name):
|
||||||
|
from certidude import authority
|
||||||
|
authority.revoke(common_name)
|
||||||
|
|
||||||
|
|
||||||
|
@click.command("cron", help="Run from cron to manage Certidude server")
|
||||||
|
def certidude_cron():
|
||||||
|
import itertools
|
||||||
|
from certidude import authority, config
|
||||||
|
now = datetime.now()
|
||||||
|
for cn, path, buf, cert, server in itertools.chain(authority.list_signed(), authority.list_revoked()):
|
||||||
|
if cert.not_valid_after < now:
|
||||||
|
expired_path = os.path.join(config.EXPIRED_DIR, "%x.pem" % cert.serial)
|
||||||
|
assert not os.path.exists(expired_path)
|
||||||
|
os.rename(path, expired_path)
|
||||||
|
click.echo("Moved %s to %s" % (path, expired_path))
|
||||||
|
|
||||||
@click.command("serve", help="Run server")
|
@click.command("serve", help="Run server")
|
||||||
@click.option("-p", "--port", default=8080 if os.getuid() else 80, help="Listen port")
|
@click.option("-p", "--port", default=8080 if os.getuid() else 80, help="Listen port")
|
||||||
@click.option("-l", "--listen", default="0.0.0.0", help="Listen address")
|
@click.option("-l", "--listen", default="0.0.0.0", help="Listen address")
|
||||||
@ -1276,9 +1208,6 @@ def certidude_setup_openvpn(): pass
|
|||||||
@click.group("setup", help="Getting started section")
|
@click.group("setup", help="Getting started section")
|
||||||
def certidude_setup(): pass
|
def certidude_setup(): pass
|
||||||
|
|
||||||
@click.group("signer", help="Signer process management")
|
|
||||||
def certidude_signer(): pass
|
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
def entry_point(): pass
|
def entry_point(): pass
|
||||||
|
|
||||||
@ -1291,15 +1220,15 @@ certidude_setup_openvpn.add_command(certidude_setup_openvpn_networkmanager)
|
|||||||
certidude_setup.add_command(certidude_setup_authority)
|
certidude_setup.add_command(certidude_setup_authority)
|
||||||
certidude_setup.add_command(certidude_setup_openvpn)
|
certidude_setup.add_command(certidude_setup_openvpn)
|
||||||
certidude_setup.add_command(certidude_setup_strongswan)
|
certidude_setup.add_command(certidude_setup_strongswan)
|
||||||
certidude_setup.add_command(certidude_setup_client)
|
|
||||||
certidude_setup.add_command(certidude_setup_nginx)
|
certidude_setup.add_command(certidude_setup_nginx)
|
||||||
entry_point.add_command(certidude_setup)
|
entry_point.add_command(certidude_setup)
|
||||||
entry_point.add_command(certidude_serve)
|
entry_point.add_command(certidude_serve)
|
||||||
entry_point.add_command(certidude_signer)
|
|
||||||
entry_point.add_command(certidude_request)
|
entry_point.add_command(certidude_request)
|
||||||
entry_point.add_command(certidude_sign)
|
entry_point.add_command(certidude_sign)
|
||||||
|
entry_point.add_command(certidude_revoke)
|
||||||
entry_point.add_command(certidude_list)
|
entry_point.add_command(certidude_list)
|
||||||
entry_point.add_command(certidude_users)
|
entry_point.add_command(certidude_users)
|
||||||
|
entry_point.add_command(certidude_cron)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
entry_point()
|
entry_point()
|
||||||
|
@ -38,27 +38,31 @@ AUTHORITY_CERTIFICATE_PATH = cp.get("authority", "certificate path")
|
|||||||
REQUESTS_DIR = cp.get("authority", "requests dir")
|
REQUESTS_DIR = cp.get("authority", "requests dir")
|
||||||
SIGNED_DIR = cp.get("authority", "signed dir")
|
SIGNED_DIR = cp.get("authority", "signed dir")
|
||||||
REVOKED_DIR = cp.get("authority", "revoked dir")
|
REVOKED_DIR = cp.get("authority", "revoked dir")
|
||||||
|
EXPIRED_DIR = cp.get("authority", "expired dir")
|
||||||
|
|
||||||
OUTBOX = cp.get("authority", "outbox uri")
|
OUTBOX = cp.get("authority", "outbox uri")
|
||||||
OUTBOX_NAME = cp.get("authority", "outbox sender name")
|
OUTBOX_NAME = cp.get("authority", "outbox sender name")
|
||||||
OUTBOX_MAIL = cp.get("authority", "outbox sender address")
|
OUTBOX_MAIL = cp.get("authority", "outbox sender address")
|
||||||
|
|
||||||
BUNDLE_FORMAT = cp.get("authority", "bundle format")
|
BUNDLE_FORMAT = cp.get("bundle", "format")
|
||||||
OPENVPN_BUNDLE_TEMPLATE = cp.get("authority", "openvpn bundle template")
|
OPENVPN_PROFILE_TEMPLATE = cp.get("bundle", "openvpn profile template")
|
||||||
|
|
||||||
USER_CERTIFICATE_ENROLLMENT = {
|
MACHINE_ENROLLMENT_ALLOWED = {
|
||||||
|
"forbidden": False, "allowed": True }[
|
||||||
|
cp.get("authority", "machine enrollment")]
|
||||||
|
USER_ENROLLMENT_ALLOWED = {
|
||||||
"forbidden": False, "single allowed": True, "multiple allowed": True }[
|
"forbidden": False, "single allowed": True, "multiple allowed": True }[
|
||||||
cp.get("authority", "user certificate enrollment")]
|
cp.get("authority", "user enrollment")]
|
||||||
USER_MULTIPLE_CERTIFICATES = {
|
USER_MULTIPLE_CERTIFICATES = {
|
||||||
"forbidden": False, "single allowed": False, "multiple allowed": True }[
|
"forbidden": False, "single allowed": False, "multiple allowed": True }[
|
||||||
cp.get("authority", "user certificate enrollment")]
|
cp.get("authority", "user enrollment")]
|
||||||
|
|
||||||
CERTIFICATE_BASIC_CONSTRAINTS = "CA:FALSE"
|
REQUEST_SUBMISSION_ALLOWED = cp.getboolean("authority", "request submission allowed")
|
||||||
CERTIFICATE_KEY_USAGE_FLAGS = "digitalSignature,keyEncipherment"
|
CLIENT_CERTIFICATE_LIFETIME = cp.getint("signature", "client certificate lifetime")
|
||||||
CERTIFICATE_EXTENDED_KEY_USAGE_FLAGS = "clientAuth"
|
SERVER_CERTIFICATE_LIFETIME = cp.getint("signature", "server certificate lifetime")
|
||||||
CERTIFICATE_LIFETIME = cp.getint("signature", "certificate lifetime")
|
AUTHORITY_CERTIFICATE_URL = cp.get("signature", "authority certificate url")
|
||||||
CERTIFICATE_AUTHORITY_URL = cp.get("signature", "certificate url")
|
|
||||||
CERTIFICATE_CRL_URL = cp.get("signature", "revoked url")
|
CERTIFICATE_CRL_URL = cp.get("signature", "revoked url")
|
||||||
|
CERTIFICATE_RENEWAL_ALLOWED = cp.getboolean("signature", "renewal allowed")
|
||||||
|
|
||||||
REVOCATION_LIST_LIFETIME = cp.getint("signature", "revocation list lifetime")
|
REVOCATION_LIST_LIFETIME = cp.getint("signature", "revocation list lifetime")
|
||||||
|
|
||||||
|
@ -2,12 +2,9 @@ import falcon
|
|||||||
import ipaddress
|
import ipaddress
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
import types
|
import types
|
||||||
from datetime import date, time, datetime
|
from datetime import date, time, datetime
|
||||||
from OpenSSL import crypto
|
|
||||||
from certidude.auth import User
|
from certidude.auth import User
|
||||||
from certidude.wrappers import Request, Certificate
|
|
||||||
from urlparse import urlparse
|
from urlparse import urlparse
|
||||||
|
|
||||||
logger = logging.getLogger("api")
|
logger = logging.getLogger("api")
|
||||||
@ -52,21 +49,7 @@ def event_source(func):
|
|||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
class MyEncoder(json.JSONEncoder):
|
class MyEncoder(json.JSONEncoder):
|
||||||
REQUEST_ATTRIBUTES = "is_client", "identity", "changed", "common_name", \
|
|
||||||
"organizational_unit", "fqdn", \
|
|
||||||
"key_type", "key_length", "md5sum", "sha1sum", "sha256sum", "key_usage"
|
|
||||||
|
|
||||||
CERTIFICATE_ATTRIBUTES = "revokable", "identity", "common_name", \
|
|
||||||
"organizational_unit", "fqdn", \
|
|
||||||
"key_type", "key_length", "sha256sum", "serial_number", "key_usage", \
|
|
||||||
"signed", "expires"
|
|
||||||
|
|
||||||
def default(self, obj):
|
def default(self, obj):
|
||||||
if isinstance(obj, crypto.X509Name):
|
|
||||||
try:
|
|
||||||
return ", ".join(["%s=%s" % (k.decode("ascii"),v.decode("utf-8")) for k, v in obj.get_components()])
|
|
||||||
except UnicodeDecodeError: # Work around old buggy pyopenssl
|
|
||||||
return ", ".join(["%s=%s" % (k.decode("ascii"),v.decode("iso8859")) for k, v in obj.get_components()])
|
|
||||||
if isinstance(obj, ipaddress._IPAddressBase):
|
if isinstance(obj, ipaddress._IPAddressBase):
|
||||||
return str(obj)
|
return str(obj)
|
||||||
if isinstance(obj, set):
|
if isinstance(obj, set):
|
||||||
@ -77,17 +60,9 @@ class MyEncoder(json.JSONEncoder):
|
|||||||
return obj.strftime("%Y-%m-%d")
|
return obj.strftime("%Y-%m-%d")
|
||||||
if isinstance(obj, types.GeneratorType):
|
if isinstance(obj, types.GeneratorType):
|
||||||
return tuple(obj)
|
return tuple(obj)
|
||||||
if isinstance(obj, Request):
|
|
||||||
return dict([(key, getattr(obj, key)) for key in self.REQUEST_ATTRIBUTES \
|
|
||||||
if hasattr(obj, key) and getattr(obj, key)])
|
|
||||||
if isinstance(obj, Certificate):
|
|
||||||
return dict([(key, getattr(obj, key)) for key in self.CERTIFICATE_ATTRIBUTES \
|
|
||||||
if hasattr(obj, key) and getattr(obj, key)])
|
|
||||||
if isinstance(obj, User):
|
if isinstance(obj, User):
|
||||||
return dict(name=obj.name, given_name=obj.given_name,
|
return dict(name=obj.name, given_name=obj.given_name,
|
||||||
surname=obj.surname, mail=obj.mail)
|
surname=obj.surname, mail=obj.mail)
|
||||||
if hasattr(obj, "serialize"):
|
|
||||||
return obj.serialize()
|
|
||||||
return json.JSONEncoder.default(self, obj)
|
return json.JSONEncoder.default(self, obj)
|
||||||
|
|
||||||
|
|
||||||
@ -96,29 +71,13 @@ def serialize(func):
|
|||||||
Falcon response serialization
|
Falcon response serialization
|
||||||
"""
|
"""
|
||||||
def wrapped(instance, req, resp, **kwargs):
|
def wrapped(instance, req, resp, **kwargs):
|
||||||
|
if not req.client_accepts("application/json"):
|
||||||
|
logger.debug("Client did not accept application/json")
|
||||||
|
raise falcon.HTTPUnsupportedMediaType(
|
||||||
|
"Client did not accept application/json")
|
||||||
resp.set_header("Cache-Control", "no-cache, no-store, must-revalidate")
|
resp.set_header("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||||
resp.set_header("Pragma", "no-cache")
|
resp.set_header("Pragma", "no-cache")
|
||||||
resp.set_header("Expires", "0")
|
resp.set_header("Expires", "0")
|
||||||
r = func(instance, req, resp, **kwargs)
|
resp.body = json.dumps(func(instance, req, resp, **kwargs), cls=MyEncoder)
|
||||||
if resp.body is None:
|
|
||||||
if req.accept.startswith("application/json"):
|
|
||||||
resp.set_header("Content-Type", "application/json")
|
|
||||||
resp.set_header("Content-Disposition", "inline")
|
|
||||||
resp.body = json.dumps(r, cls=MyEncoder)
|
|
||||||
elif hasattr(r, "content_type") and req.client_accepts(r.content_type):
|
|
||||||
resp.set_header("Content-Type", r.content_type)
|
|
||||||
resp.set_header("Content-Disposition",
|
|
||||||
("attachment; filename=%s" % r.suggested_filename).encode("ascii"))
|
|
||||||
resp.body = r.dump()
|
|
||||||
elif hasattr(r, "content_type"):
|
|
||||||
logger.debug(u"Client did not accept application/json or %s, "
|
|
||||||
"client expected %s", r.content_type, req.accept)
|
|
||||||
raise falcon.HTTPUnsupportedMediaType(
|
|
||||||
"Client did not accept application/json or %s" % r.content_type)
|
|
||||||
else:
|
|
||||||
logger.debug(u"Client did not accept application/json, client expected %s", req.accept)
|
|
||||||
raise falcon.HTTPUnsupportedMediaType(
|
|
||||||
"Client did not accept application/json")
|
|
||||||
return r
|
|
||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
|
@ -4,18 +4,20 @@ import os
|
|||||||
import requests
|
import requests
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from base64 import b64encode
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from certidude import errors, const
|
from certidude import errors, const
|
||||||
from certidude.wrappers import Certificate, Request
|
|
||||||
from cryptography import x509
|
from cryptography import x509
|
||||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
from cryptography.hazmat.primitives.asymmetric import rsa, padding
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
from cryptography.hazmat.primitives import hashes, serialization
|
from cryptography.hazmat.primitives import hashes, serialization
|
||||||
from cryptography.hazmat.primitives.serialization import Encoding
|
from cryptography.hazmat.primitives.serialization import Encoding
|
||||||
from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID, AuthorityInformationAccessOID
|
from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID, AuthorityInformationAccessOID
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
from OpenSSL import crypto
|
from cryptography import x509
|
||||||
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
|
||||||
def certidude_request_certificate(server, key_path, request_path, certificate_path, authority_path, revocations_path, common_name, autosign=False, wait=False, ip_address=None, bundle=False, insecure=False):
|
def certidude_request_certificate(server, key_path, request_path, certificate_path, authority_path, revocations_path, common_name, autosign=False, wait=False, bundle=False, insecure=False):
|
||||||
"""
|
"""
|
||||||
Exchange CSR for certificate using Certidude HTTP API server
|
Exchange CSR for certificate using Certidude HTTP API server
|
||||||
"""
|
"""
|
||||||
@ -26,6 +28,8 @@ def certidude_request_certificate(server, key_path, request_path, certificate_pa
|
|||||||
if wait:
|
if wait:
|
||||||
request_params.add("wait=forever")
|
request_params.add("wait=forever")
|
||||||
|
|
||||||
|
renew = False # Attempt to renew if certificate has expired
|
||||||
|
|
||||||
# Expand ca.example.com
|
# Expand ca.example.com
|
||||||
scheme = "http" if insecure else "https" # TODO: Expose in CLI
|
scheme = "http" if insecure else "https" # TODO: Expose in CLI
|
||||||
authority_url = "%s://%s/api/certificate/" % (scheme, server)
|
authority_url = "%s://%s/api/certificate/" % (scheme, server)
|
||||||
@ -41,13 +45,14 @@ def certidude_request_certificate(server, key_path, request_path, certificate_pa
|
|||||||
click.echo("Attempting to fetch authority certificate from %s" % authority_url)
|
click.echo("Attempting to fetch authority certificate from %s" % authority_url)
|
||||||
try:
|
try:
|
||||||
r = requests.get(authority_url,
|
r = requests.get(authority_url,
|
||||||
headers={"Accept": "application/x-x509-ca-cert,application/x-pem-file"})
|
headers={"Accept": "application/x-x509-ca-cert,application/x-pem-file"})
|
||||||
cert = crypto.load_certificate(crypto.FILETYPE_PEM, r.text)
|
x509.load_pem_x509_certificate(r.content, default_backend())
|
||||||
except crypto.Error:
|
except:
|
||||||
raise ValueError("Failed to parse PEM: %s" % r.text)
|
raise
|
||||||
|
# raise ValueError("Failed to parse PEM: %s" % r.text)
|
||||||
authority_partial = tempfile.mktemp(prefix=authority_path + ".part")
|
authority_partial = tempfile.mktemp(prefix=authority_path + ".part")
|
||||||
with open(authority_partial, "w") as oh:
|
with open(authority_partial, "w") as oh:
|
||||||
oh.write(r.text)
|
oh.write(r.content)
|
||||||
click.echo("Writing authority certificate to: %s" % authority_path)
|
click.echo("Writing authority certificate to: %s" % authority_path)
|
||||||
os.rename(authority_partial, authority_path)
|
os.rename(authority_partial, authority_path)
|
||||||
|
|
||||||
@ -68,18 +73,19 @@ def certidude_request_certificate(server, key_path, request_path, certificate_pa
|
|||||||
|
|
||||||
# Check if we have been inserted into CRL
|
# Check if we have been inserted into CRL
|
||||||
if os.path.exists(certificate_path):
|
if os.path.exists(certificate_path):
|
||||||
cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(certificate_path).read())
|
cert = x509.load_pem_x509_certificate(open(certificate_path).read(), default_backend())
|
||||||
revocation_list = crypto.load_crl(crypto.FILETYPE_PEM, open(revocations_path).read())
|
|
||||||
for revocation in revocation_list.get_revoked():
|
for revocation in x509.load_pem_x509_crl(open(revocations_path).read(), default_backend()):
|
||||||
if int(revocation.get_serial(), 16) == cert.get_serial_number():
|
extension, = revocation.extensions
|
||||||
if revocation.get_reason() == "Certificate Hold": # TODO: 'Remove From CRL'
|
|
||||||
# TODO: Disable service for time being
|
if revocation.serial_number == cert.serial_number:
|
||||||
click.echo("Certificate put on hold, doing nothing for now")
|
if extension.value.reason == x509.ReasonFlags.certificate_hold:
|
||||||
|
# Don't do anything for now
|
||||||
|
# TODO: disable service
|
||||||
break
|
break
|
||||||
|
|
||||||
# Disable the client if operation has been ceased or
|
# Disable the client if operation has been ceased
|
||||||
# the certificate has been superseded by other
|
if extension.value.reason == x509.ReasonFlags.cessation_of_operation:
|
||||||
if revocation.get_reason() in ("Cessation Of Operation", "Superseded"):
|
|
||||||
if os.path.exists("/etc/certidude/client.conf"):
|
if os.path.exists("/etc/certidude/client.conf"):
|
||||||
clients.readfp(open("/etc/certidude/client.conf"))
|
clients.readfp(open("/etc/certidude/client.conf"))
|
||||||
if clients.has_section(server):
|
if clients.has_section(server):
|
||||||
@ -87,9 +93,7 @@ def certidude_request_certificate(server, key_path, request_path, certificate_pa
|
|||||||
clients.write(open("/etc/certidude/client.conf", "w"))
|
clients.write(open("/etc/certidude/client.conf", "w"))
|
||||||
click.echo("Authority operation ceased, disabling in /etc/certidude/client.conf")
|
click.echo("Authority operation ceased, disabling in /etc/certidude/client.conf")
|
||||||
# TODO: Disable related services
|
# TODO: Disable related services
|
||||||
if revocation.get_reason() in ("CA Compromise", "AA Compromise"):
|
return
|
||||||
if os.path.exists(authority_path):
|
|
||||||
os.remove(key_path)
|
|
||||||
|
|
||||||
click.echo("Certificate has been revoked, wiping keys and certificates!")
|
click.echo("Certificate has been revoked, wiping keys and certificates!")
|
||||||
if os.path.exists(key_path):
|
if os.path.exists(key_path):
|
||||||
@ -102,9 +106,16 @@ def certidude_request_certificate(server, key_path, request_path, certificate_pa
|
|||||||
else:
|
else:
|
||||||
click.echo("Certificate does not seem to be revoked. Good!")
|
click.echo("Certificate does not seem to be revoked. Good!")
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
request = Request(open(request_path))
|
request_buf = open(request_path).read()
|
||||||
|
request = x509.load_pem_x509_csr(request_buf, default_backend())
|
||||||
click.echo("Found signing request: %s" % request_path)
|
click.echo("Found signing request: %s" % request_path)
|
||||||
|
with open(key_path) as fh:
|
||||||
|
key = serialization.load_pem_private_key(
|
||||||
|
fh.read(),
|
||||||
|
password=None,
|
||||||
|
backend=default_backend())
|
||||||
except EnvironmentError:
|
except EnvironmentError:
|
||||||
|
|
||||||
# Construct private key
|
# Construct private key
|
||||||
@ -146,9 +157,16 @@ def certidude_request_certificate(server, key_path, request_path, certificate_pa
|
|||||||
# Update CRL, renew certificate, maybe something extra?
|
# Update CRL, renew certificate, maybe something extra?
|
||||||
|
|
||||||
if os.path.exists(certificate_path):
|
if os.path.exists(certificate_path):
|
||||||
click.echo("Found certificate: %s" % certificate_path)
|
cert_buf = open(certificate_path).read()
|
||||||
# TODO: Check certificate validity, download CRL?
|
cert = x509.load_pem_x509_certificate(cert_buf, default_backend())
|
||||||
return
|
lifetime = (cert.not_valid_after - cert.not_valid_before)
|
||||||
|
rollover = lifetime / 1 # TODO: Make rollover configurable
|
||||||
|
if datetime.now() > cert.not_valid_after - rollover:
|
||||||
|
click.echo("Certificate expired %s" % cert.not_valid_after)
|
||||||
|
renew = True
|
||||||
|
else:
|
||||||
|
click.echo("Found valid certificate: %s" % certificate_path)
|
||||||
|
return
|
||||||
|
|
||||||
# If machine is joined to domain attempt to present machine credentials for authentication
|
# If machine is joined to domain attempt to present machine credentials for authentication
|
||||||
if os.path.exists("/etc/krb5.keytab"):
|
if os.path.exists("/etc/krb5.keytab"):
|
||||||
@ -169,10 +187,25 @@ def certidude_request_certificate(server, key_path, request_path, certificate_pa
|
|||||||
auth = None
|
auth = None
|
||||||
|
|
||||||
click.echo("Submitting to %s, waiting for response..." % request_url)
|
click.echo("Submitting to %s, waiting for response..." % request_url)
|
||||||
submission = requests.post(request_url,
|
headers={
|
||||||
auth=auth,
|
"Content-Type": "application/pkcs10",
|
||||||
data=open(request_path),
|
"Accept": "application/x-x509-user-cert,application/x-pem-file"
|
||||||
headers={"Content-Type": "application/pkcs10", "Accept": "application/x-x509-user-cert,application/x-pem-file"})
|
}
|
||||||
|
|
||||||
|
if renew:
|
||||||
|
signer = key.signer(
|
||||||
|
padding.PSS(
|
||||||
|
mgf=padding.MGF1(hashes.SHA512()),
|
||||||
|
salt_length=padding.PSS.MAX_LENGTH
|
||||||
|
),
|
||||||
|
hashes.SHA512()
|
||||||
|
)
|
||||||
|
signer.update(cert_buf)
|
||||||
|
signer.update(request_buf)
|
||||||
|
headers["X-Renewal-Signature"] = b64encode(signer.finalize())
|
||||||
|
click.echo("Attached renewal signature %s" % headers["X-Renewal-Signature"])
|
||||||
|
|
||||||
|
submission = requests.post(request_url, auth=auth, data=open(request_path), headers=headers)
|
||||||
|
|
||||||
# Destroy service ticket
|
# Destroy service ticket
|
||||||
if os.path.exists("/tmp/ca.ticket"):
|
if os.path.exists("/tmp/ca.ticket"):
|
||||||
@ -192,8 +225,8 @@ def certidude_request_certificate(server, key_path, request_path, certificate_pa
|
|||||||
submission.raise_for_status()
|
submission.raise_for_status()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cert = crypto.load_certificate(crypto.FILETYPE_PEM, submission.text)
|
cert = x509.load_pem_x509_certificate(submission.text.encode("ascii"), default_backend())
|
||||||
except crypto.Error:
|
except: # TODO: catch correct exceptions
|
||||||
raise ValueError("Failed to parse PEM: %s" % submission.text)
|
raise ValueError("Failed to parse PEM: %s" % submission.text)
|
||||||
|
|
||||||
os.umask(0o022)
|
os.umask(0o022)
|
||||||
|
@ -79,10 +79,10 @@ def send(template, to=None, attachments=(), **context):
|
|||||||
msg.attach(part1)
|
msg.attach(part1)
|
||||||
msg.attach(part2)
|
msg.attach(part2)
|
||||||
|
|
||||||
for attachment in attachments:
|
for attachment, content_type, suggested_filename in attachments:
|
||||||
part = MIMEBase(*attachment.content_type.split("/"))
|
part = MIMEBase(*content_type.split("/"))
|
||||||
part.add_header('Content-Disposition', 'attachment', filename=attachment.suggested_filename)
|
part.add_header('Content-Disposition', 'attachment', filename=suggested_filename)
|
||||||
part.set_payload(attachment.dump())
|
part.set_payload(attachment)
|
||||||
msg.attach(part)
|
msg.attach(part)
|
||||||
|
|
||||||
# Gmail employs some sort of IPS
|
# Gmail employs some sort of IPS
|
||||||
|
@ -29,7 +29,8 @@ class SignHandler(asynchat.async_chat):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
builder = x509.CertificateRevocationListBuilder(
|
builder = x509.CertificateRevocationListBuilder(
|
||||||
).last_update(now
|
).last_update(
|
||||||
|
now - timedelta(minutes=5)
|
||||||
).next_update(
|
).next_update(
|
||||||
now + timedelta(seconds=config.REVOCATION_LIST_LIFETIME)
|
now + timedelta(seconds=config.REVOCATION_LIST_LIFETIME)
|
||||||
).issuer_name(self.server.certificate.issuer
|
).issuer_name(self.server.certificate.issuer
|
||||||
@ -89,9 +90,12 @@ class SignHandler(asynchat.async_chat):
|
|||||||
).public_key(
|
).public_key(
|
||||||
request.public_key()
|
request.public_key()
|
||||||
).not_valid_before(
|
).not_valid_before(
|
||||||
now - timedelta(hours=1)
|
now
|
||||||
).not_valid_after(
|
).not_valid_after(
|
||||||
now + timedelta(days=config.CERTIFICATE_LIFETIME)
|
now + timedelta(days=
|
||||||
|
config.SERVER_CERTIFICATE_LIFETIME
|
||||||
|
if server_flags
|
||||||
|
else config.CLIENT_CERTIFICATE_LIFETIME)
|
||||||
).add_extension(
|
).add_extension(
|
||||||
x509.BasicConstraints(
|
x509.BasicConstraints(
|
||||||
ca=False,
|
ca=False,
|
||||||
@ -122,7 +126,7 @@ class SignHandler(asynchat.async_chat):
|
|||||||
x509.AccessDescription(
|
x509.AccessDescription(
|
||||||
AuthorityInformationAccessOID.CA_ISSUERS,
|
AuthorityInformationAccessOID.CA_ISSUERS,
|
||||||
x509.UniformResourceIdentifier(
|
x509.UniformResourceIdentifier(
|
||||||
config.CERTIFICATE_AUTHORITY_URL)
|
config.AUTHORITY_CERTIFICATE_URL)
|
||||||
)
|
)
|
||||||
]),
|
]),
|
||||||
critical=False
|
critical=False
|
||||||
|
1
certidude/static/img/iconmonstr-server-1.svg
Normal file
1
certidude/static/img/iconmonstr-server-1.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M13 19h-2v2h-4v2h10v-2h-4v-2zm9 2h-4v2h4v-2zm-16 0h-4v2h4v-2zm18-11h-24v7h24v-7zm-22 5l.863-3h1.275l-.863 3h-1.275zm2.066 0l.863-3h1.275l-.863 3h-1.275zm2.067 0l.863-3h1.275l-.864 3h-1.274zm2.066 0l.863-3h1.274l-.863 3h-1.274zm3.341 0h-1.275l.864-3h1.275l-.864 3zm9.46-.5c-.552 0-1-.448-1-1s.448-1 1-1 1 .448 1 1-.448 1-1 1zm1-6.5h-20l4-7h12l4 7z"/></svg>
|
After Width: | Height: | Size: 447 B |
@ -108,18 +108,23 @@ function onClientDown(e) {
|
|||||||
|
|
||||||
function onRequestSigned(e) {
|
function onRequestSigned(e) {
|
||||||
console.log("Request signed:", e.data);
|
console.log("Request signed:", e.data);
|
||||||
|
var slug = e.data.replace("@", "--").replace(".", "-");
|
||||||
|
console.log("Removing:", slug);
|
||||||
|
|
||||||
$("#request-" + e.data.replace("@", "--").replace(".", "-")).slideUp("normal", function() { $(this).remove(); });
|
$("#request-" + slug).slideUp("normal", function() { $(this).remove(); });
|
||||||
$("#certificate-" + e.data.replace("@", "--").replace(".", "-")).slideUp("normal", function() { $(this).remove(); });
|
$("#certificate-" + slug).slideUp("normal", function() { $(this).remove(); });
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/api/signed/" + e.data + "/",
|
url: "/api/signed/" + e.data + "/",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
success: function(certificate, status, xhr) {
|
success: function(certificate, status, xhr) {
|
||||||
console.info(certificate);
|
console.info("Retrieved certificate:", certificate);
|
||||||
$("#signed_certificates").prepend(
|
$("#signed_certificates").prepend(
|
||||||
nunjucks.render('views/signed.html', { certificate: certificate }));
|
nunjucks.render('views/signed.html', { certificate: certificate }));
|
||||||
|
},
|
||||||
|
error: function(response) {
|
||||||
|
console.info("Failed to retrieve certificate:", response);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -234,7 +239,7 @@ $(document).ready(function() {
|
|||||||
$(window).on("search", function() {
|
$(window).on("search", function() {
|
||||||
var q = $("#search").val();
|
var q = $("#search").val();
|
||||||
$(".filterable").each(function(i, e) {
|
$(".filterable").each(function(i, e) {
|
||||||
if ($(e).attr("data-dn").toLowerCase().indexOf(q) >= 0) {
|
if ($(e).attr("data-cn").toLowerCase().indexOf(q) >= 0) {
|
||||||
$(e).show();
|
$(e).show();
|
||||||
} else {
|
} else {
|
||||||
$(e).hide();
|
$(e).hide();
|
||||||
|
@ -2,12 +2,10 @@
|
|||||||
<section id="about">
|
<section id="about">
|
||||||
<h2>{{ session.user.gn }} {{ session.user.sn }} ({{session.user.name }}) settings</h2>
|
<h2>{{ session.user.gn }} {{ session.user.sn }} ({{session.user.name }}) settings</h2>
|
||||||
|
|
||||||
<p>Mails will be sent to: {{ session.user.mail }}</p>
|
<p title="Bundles are mainly intended for Android and iOS users">
|
||||||
|
Click <a href="/api/bundle/">here</a> to generate Android or iOS bundle for current user account.</p>
|
||||||
|
|
||||||
{% if session.authority.user_certificate_enrollment %}
|
<p>Mails will be sent to: {{ session.user.mail }}</p>
|
||||||
<p>You can click <a href="/api/bundle/">here</a> to generate bundle
|
|
||||||
for current user account.</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if session.authority %}
|
{% if session.authority %}
|
||||||
|
|
||||||
@ -28,9 +26,9 @@ as such require complete reset of X509 infrastructure if some of them needs to b
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
<p>User certificate enrollment:
|
<p>User enrollment:
|
||||||
{% if session.authority.user_certificate_enrollment %}
|
{% if session.authority.user_enrollment_allowed %}
|
||||||
{% if session.authority.user_mutliple_certificates %}
|
{% if session.authority.user_multiple_certificates %}
|
||||||
multiple
|
multiple
|
||||||
{% else %}
|
{% else %}
|
||||||
single
|
single
|
||||||
@ -42,10 +40,20 @@ forbidden
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
<p>Web signed certificate attributes:</p>
|
<p>Machine enrollment:
|
||||||
|
{% if session.authority.machine_enrollment_allowed %}
|
||||||
|
allowed
|
||||||
|
{% else %}
|
||||||
|
forbidden
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<p>Certificate attributes:</p>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li>Certificate lifetime: {{ session.authority.signature.certificate_lifetime }} days</li>
|
<li>Server certificate lifetime: {{ session.authority.signature.server_certificate_lifetime }} days</li>
|
||||||
|
<li>Client certificate lifetime: {{ session.authority.signature.client_certificate_lifetime }} days</li>
|
||||||
<li>Revocation list lifetime: {{ session.authority.signature.revocation_list_lifetime }} seconds</li>
|
<li>Revocation list lifetime: {{ session.authority.signature.revocation_list_lifetime }} seconds</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
@ -134,13 +142,13 @@ cat example.csr
|
|||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
<p>Paste the contents here and click submit:</p>
|
<p>Paste the contents here and click submit:</p>
|
||||||
<textarea id="request_body" style="width:100%; min-height: 4em;" placeholder="-----BEGIN CERTIFICATE REQUEST-----
|
<textarea id="request_body" style="width:100%; min-height: 4em;" placeholder="-----BEGIN CERTIFICATE REQUEST-----"></textarea>
|
||||||
...
|
|
||||||
-----END CERTIFICATE REQUEST-----"></textarea>
|
|
||||||
<button class="icon upload" id="request_submit" style="float:none;">Submit</button>
|
<button class="icon upload" id="request_submit" style="float:none;">Submit</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>Submit a certificate signing request with Certidude:</p>
|
<p>Submit a certificate signing request from Mac OS X, Ubuntu or Fedora:</p>
|
||||||
<pre>certidude setup client {{session.common_name}}</pre>
|
<pre>easy_install pip
|
||||||
|
pip install certidude
|
||||||
|
certidude bootstrap {{session.authority.common_name}}</pre>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<ul id="pending_requests">
|
<ul id="pending_requests">
|
||||||
@ -180,7 +188,8 @@ cat example.csr
|
|||||||
<h1>Revoked certificates</h1>
|
<h1>Revoked certificates</h1>
|
||||||
<p>To fetch <a href="{{window.location.href}}api/revoked/">certificate revocation list</a>:</p>
|
<p>To fetch <a href="{{window.location.href}}api/revoked/">certificate revocation list</a>:</p>
|
||||||
<pre>curl {{window.location.href}}api/revoked/ > crl.der
|
<pre>curl {{window.location.href}}api/revoked/ > crl.der
|
||||||
curl http://ca2.koodur.lan/api/revoked/?wait=yes -H "Accept: application/x-pem-file" > crl.pem</pre>
|
curl http://ca2.koodur.lan/api/revoked/ -L -H "Accept: application/x-pem-file"
|
||||||
|
curl http://ca2.koodur.lan/api/revoked/?wait=yes -L -H "Accept: application/x-pem-file" > crl.pem</pre>
|
||||||
<!--
|
<!--
|
||||||
<p>To perform online certificate status request</p>
|
<p>To perform online certificate status request</p>
|
||||||
|
|
||||||
|
@ -1,13 +1,18 @@
|
|||||||
<li id="request-{{ request.common_name | replace('@', '--') | replace('.', '-') }}" class="filterable">
|
<li id="request-{{ request.common_name | replace('@', '--') | replace('.', '-') }}" class="filterable">
|
||||||
|
|
||||||
<a class="button icon download" href="/api/request/{{request.common_name}}/">Fetch</a>
|
<a class="button icon download" href="/api/request/{{request.common_name}}/">Fetch</a>
|
||||||
<button class="icon sign" onClick="javascript:$(this).addClass('busy');$.ajax({url:'/api/request/{{request.common_name}}/',type:'patch'});">Sign</button>
|
<button class="icon sign" onClick="javascript:$(this).addClass('busy');$.ajax({url:'/api/request/{{request.common_name}}/?sha256sum={{ request.sha256sum }}',type:'patch'});">Sign</button>
|
||||||
<button class="icon revoke" onClick="javascript:$(this).addClass('busy');$.ajax({url:'/api/request/{{request.common_name}}/',type:'delete'});">Delete</button>
|
<button class="icon revoke" onClick="javascript:$(this).addClass('busy');$.ajax({url:'/api/request/{{request.common_name}}/?sha256sum={{ request.sha256sum }}',type:'delete'});">Delete</button>
|
||||||
|
|
||||||
|
|
||||||
<div class="monospace">
|
<div class="monospace">
|
||||||
|
{% if request.server %}
|
||||||
|
{% include 'img/iconmonstr-server-1.svg' %}
|
||||||
|
{% else %}
|
||||||
{% include 'img/iconmonstr-certificate-15.svg' %}
|
{% include 'img/iconmonstr-certificate-15.svg' %}
|
||||||
{{request.identity}}
|
{% endif %}
|
||||||
|
|
||||||
|
{{request.common_name}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if request.email_address %}
|
{% if request.email_address %}
|
||||||
@ -16,7 +21,7 @@
|
|||||||
|
|
||||||
<div class="monospace">
|
<div class="monospace">
|
||||||
{% include 'img/iconmonstr-key-3.svg' %}
|
{% include 'img/iconmonstr-key-3.svg' %}
|
||||||
<span title="SHA-1 of public key">
|
<span title="SHA-256 of certificate signing request">
|
||||||
{{ request.sha256sum }}
|
{{ request.sha256sum }}
|
||||||
</span>
|
</span>
|
||||||
{{ request.key_length }}-bit
|
{{ request.key_length }}-bit
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
<li id="certificate-{{ certificate.common_name | replace('@', '--') | replace('.', '-') }}" data-dn="{{ certificate.identity }}" data-cn="{{ certificate.common_name }}" class="filterable">
|
<li id="certificate-{{ certificate.common_name | replace('@', '--') | replace('.', '-') }}" data-dn="CN={{ certificate.common_name }}" data-cn="{{ certificate.common_name }}" class="filterable">
|
||||||
<a class="button icon download" href="/api/signed/{{certificate.common_name}}/">Fetch</a>
|
<a class="button icon download" href="/api/signed/{{certificate.common_name}}/">Fetch</a>
|
||||||
<button class="icon revoke" onClick="javascript:$(this).addClass('busy');$.ajax({url:'/api/signed/{{certificate.common_name}}/',type:'delete'});">Revoke</button>
|
<button class="icon revoke" onClick="javascript:$(this).addClass('busy');$.ajax({url:'/api/signed/{{certificate.common_name}}/',type:'delete'});">Revoke</button>
|
||||||
|
|
||||||
<div class="monospace">
|
<div class="monospace">
|
||||||
|
{% if certificate.server %}
|
||||||
|
{% include 'img/iconmonstr-server-1.svg' %}
|
||||||
|
{% else %}
|
||||||
{% include 'img/iconmonstr-certificate-15.svg' %}
|
{% include 'img/iconmonstr-certificate-15.svg' %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{{certificate.common_name}}
|
{{certificate.common_name}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -78,11 +78,33 @@ database = sqlite://{{ directory }}/db.sqlite
|
|||||||
openvpn status uri = http://router.example.com/status.log
|
openvpn status uri = http://router.example.com/status.log
|
||||||
|
|
||||||
[signature]
|
[signature]
|
||||||
certificate lifetime = {{ certificate_lifetime }}
|
# Server certificate is granted to certificate with
|
||||||
revocation list lifetime = {{ revocation_list_lifetime }}
|
# common name that includes period which translates to FQDN of the machine.
|
||||||
certificate url = {{ certificate_url }}
|
# TLS Server Auth and IKE Intermediate flags are attached to such certificate.
|
||||||
|
# Due to problematic CRL support in client applications
|
||||||
|
# we keep server certificate lifetime short and
|
||||||
|
# have it renewed automatically.
|
||||||
|
server certificate lifetime = 3
|
||||||
|
|
||||||
|
# Client certificates are granted to everything else
|
||||||
|
# TLS Client Auth flag is attached to such certificate.
|
||||||
|
# In this case it's set to 4 months.
|
||||||
|
client certificate lifetime = 120
|
||||||
|
|
||||||
|
revocation list lifetime = 24
|
||||||
|
|
||||||
|
# URL where CA certificate can be fetched from
|
||||||
|
authority certificate url = {{ certificate_url }}
|
||||||
|
|
||||||
|
# Strongswan can be configured to automatically fetch CRL
|
||||||
|
# in that case CRL URL has to be embedded in the certificate
|
||||||
revoked url = {{ revoked_url }}
|
revoked url = {{ revoked_url }}
|
||||||
|
|
||||||
|
# If certificate renewal is allowed clients can request a certificate
|
||||||
|
# for the same public key with extended lifetime
|
||||||
|
renewal allowed = false
|
||||||
|
;renewal allowed = true
|
||||||
|
|
||||||
[push]
|
[push]
|
||||||
event source token = {{ push_token }}
|
event source token = {{ push_token }}
|
||||||
event source subscribe = {{ push_server }}/ev/sub/%s
|
event source subscribe = {{ push_server }}/ev/sub/%s
|
||||||
@ -91,14 +113,25 @@ long poll subscribe = {{ push_server }}/lp/sub/%s
|
|||||||
long poll publish = {{ push_server }}/lp/pub/%s
|
long poll publish = {{ push_server }}/lp/pub/%s
|
||||||
|
|
||||||
[authority]
|
[authority]
|
||||||
|
# Present form for CSR submission for logged in users
|
||||||
|
;request submission allowed = true
|
||||||
|
request submission allowed = false
|
||||||
|
|
||||||
# User certificate enrollment specifies whether logged in users are allowed to
|
# User certificate enrollment specifies whether logged in users are allowed to
|
||||||
# request bundles. In case of 'single allowed' the common name of the
|
# request bundles. In case of 'single allowed' the common name of the
|
||||||
# certificate is set to username, this should work well with REMOTE_USER
|
# certificate is set to username, this should work well with REMOTE_USER
|
||||||
# enabled web apps running behind Apache/nginx.
|
# enabled web apps running behind Apache/nginx.
|
||||||
# In case of 'multiple allowed' the common name is set to username@device-identifier.
|
# In case of 'multiple allowed' the common name is set to username@device-identifier.
|
||||||
;user certificate enrollment = forbidden
|
;user enrollment = forbidden
|
||||||
;user certificate enrollment = single allowed
|
;user enrollment = single allowed
|
||||||
user certificate enrollment = multiple allowed
|
user enrollment = multiple allowed
|
||||||
|
|
||||||
|
# Machine certificate enrollment specifies whether Kerberos authenticated
|
||||||
|
# machines are allowed to automatically enroll with certificate where
|
||||||
|
# common name is set to machine's account name
|
||||||
|
machine enrollment = forbidden
|
||||||
|
;machine enrollment = allowed
|
||||||
|
|
||||||
|
|
||||||
private key path = {{ ca_key }}
|
private key path = {{ ca_key }}
|
||||||
certificate path = {{ ca_crt }}
|
certificate path = {{ ca_crt }}
|
||||||
@ -112,8 +145,10 @@ outbox uri = {{ outbox }}
|
|||||||
outbox sender name = Certificate management
|
outbox sender name = Certificate management
|
||||||
outbox sender address = certificates@example.com
|
outbox sender address = certificates@example.com
|
||||||
|
|
||||||
bundle format = p12
|
[bundle]
|
||||||
;bundle format = ovpn
|
format = p12
|
||||||
|
;format = ovpn
|
||||||
openvpn bundle template = /etc/certidude/template.ovpn
|
|
||||||
|
|
||||||
|
# Template for OpenVPN profile, copy certidude/templates/openvpn-client.conf
|
||||||
|
# to /etc/certidude/ and make modifications as necessary
|
||||||
|
openvpn profile template = {{ openvpn_profile_template_path }}
|
||||||
|
9
certidude/templates/mail/certificate-renewed.md
Normal file
9
certidude/templates/mail/certificate-renewed.md
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
Renewed {{ common_name }} ({{ serial_number }})
|
||||||
|
|
||||||
|
This is simply to notify that certificate for {{ common_name }}
|
||||||
|
was renewed and the serial number of the new certificate is {{ serial_number }}.
|
||||||
|
|
||||||
|
The new certificate is valid from {{ certificate.not_valid_before }} until
|
||||||
|
{{ certificate.not_valid_after }}.
|
||||||
|
|
||||||
|
Services making use of those certificates should continue working as expected.
|
@ -1,6 +1,6 @@
|
|||||||
Certificate {{certificate.common_name}} ({{certificate.serial_number}}) revoked
|
Revoked {{ common_name }} ({{ serial_number }})
|
||||||
|
|
||||||
This is simply to notify that certificate {{ certificate.common_name }}
|
This is simply to notify that certificate {{ common_name }}
|
||||||
was revoked.
|
was revoked.
|
||||||
|
|
||||||
Services making use of this certificates might become unavailable.
|
Services making use of this certificates might become unavailable.
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
Certificate {{certificate.common_name}} ({{certificate.serial_number}}) signed
|
Signed {{ common_name }} ({{ serial_number }})
|
||||||
|
|
||||||
This is simply to notify that certificate {{ certificate.common_name }}
|
This is simply to notify that certificate {{ common_name }}
|
||||||
|
with serial number {{ serial_number }}
|
||||||
was signed{% if signer %} by {{ signer }}{% endif %}.
|
was signed{% if signer %} by {{ signer }}{% endif %}.
|
||||||
|
|
||||||
|
The certificate is valid from {{ certificate.not_valid_before }} until
|
||||||
|
{{ certificate.not_valid_after }}.
|
||||||
|
|
||||||
Any existing certificates with the same common name were rejected by doing so
|
Any existing certificates with the same common name were rejected by doing so
|
||||||
and services making use of those certificates might become unavailable.
|
and services making use of those certificates might become unavailable.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
Certificate signing request {{request.common_name}} stored
|
Stored request {{ common_name }}
|
||||||
|
|
||||||
This is simply to notify that certificate signing request for {{ request.common_name }}
|
This is simply to notify that certificate signing request for {{ common_name }}
|
||||||
was stored. You may log in with a certificate authority administration account to sign it.
|
was stored. You may log in with a certificate authority administration account to sign it.
|
||||||
|
|
||||||
|
5
certidude/templates/mail/token.md
Normal file
5
certidude/templates/mail/token.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
Stored request {{ common_name }}
|
||||||
|
|
||||||
|
This is simply to notify that certificate signing request for {{ common_name }}
|
||||||
|
was stored. You may log in with a certificate authority administration account to sign it.
|
||||||
|
|
@ -1,5 +1,6 @@
|
|||||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
# Following are already enabled by /etc/nginx/nginx.conf
|
||||||
ssl_prefer_server_ciphers on;
|
#ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||||
|
#ssl_prefer_server_ciphers on;
|
||||||
ssl_session_cache shared:SSL:10m;
|
ssl_session_cache shared:SSL:10m;
|
||||||
ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
|
ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
|
||||||
ssl_dhparam {{dhparam_path}};
|
ssl_dhparam {{dhparam_path}};
|
||||||
|
@ -7,16 +7,22 @@ nobind
|
|||||||
# OpenVPN gateway(s), uncomment remote-random to load balance
|
# OpenVPN gateway(s), uncomment remote-random to load balance
|
||||||
comp-lzo
|
comp-lzo
|
||||||
proto udp
|
proto udp
|
||||||
remote 1.2.3.4
|
{% if servers %}
|
||||||
;remote 1.2.3.5
|
remote-random
|
||||||
;remote-random
|
{% for server in servers %}
|
||||||
|
remote {{ server }} 51900
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
remote 1.2.3.4 1194
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
# Virtual network interface settings
|
# Virtual network interface settings
|
||||||
dev tun
|
dev tun
|
||||||
persist-tun
|
persist-tun
|
||||||
|
|
||||||
# Customize crypto settings
|
# Customize crypto settings
|
||||||
;tls-cipher TLS-DHE-RSA-WITH-AES-256-CBC-SHA384
|
;tls-version-min 1.2
|
||||||
|
;tls-cipher TLS-DHE-RSA-WITH-AES-256-GCM-SHA384
|
||||||
;cipher AES-256-CBC
|
;cipher AES-256-CBC
|
||||||
;auth SHA384
|
;auth SHA384
|
||||||
|
|
||||||
@ -36,12 +42,12 @@ persist-key
|
|||||||
</cert>
|
</cert>
|
||||||
|
|
||||||
# Revocation list
|
# Revocation list
|
||||||
<crl-verify>
|
# Tunnelblick doens't handle inlined CRL
|
||||||
{{crl}}
|
# hard to update as well
|
||||||
</crl-verify>
|
;<crl-verify>
|
||||||
|
;</crl-verify>
|
||||||
|
|
||||||
# Pre-shared key for extra layer of security
|
# Pre-shared key for extra layer of security
|
||||||
;<ta>
|
;<ta>
|
||||||
;...
|
|
||||||
;</ta>
|
;</ta>
|
||||||
|
|
@ -84,8 +84,6 @@ class DirectoryConnection(object):
|
|||||||
class ActiveDirectoryUserManager(object):
|
class ActiveDirectoryUserManager(object):
|
||||||
def get(self, username):
|
def get(self, username):
|
||||||
# TODO: Sanitize username
|
# TODO: Sanitize username
|
||||||
if "@" in username:
|
|
||||||
username, _ = username.split("@", 1)
|
|
||||||
with DirectoryConnection() as conn:
|
with DirectoryConnection() as conn:
|
||||||
ft = config.LDAP_USER_FILTER % username
|
ft = config.LDAP_USER_FILTER % username
|
||||||
attribs = "cn", "givenName", "sn", "mail", "userPrincipalName"
|
attribs = "cn", "givenName", "sn", "mail", "userPrincipalName"
|
||||||
|
@ -1,248 +0,0 @@
|
|||||||
import os
|
|
||||||
import hashlib
|
|
||||||
import re
|
|
||||||
import click
|
|
||||||
import io
|
|
||||||
from certidude import const
|
|
||||||
from OpenSSL import crypto
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
def subject2dn(subject):
|
|
||||||
bits = []
|
|
||||||
for j in "CN", "GN", "SN", "C", "S", "L", "O", "OU":
|
|
||||||
if getattr(subject, j, None):
|
|
||||||
bits.append("%s=%s" % (j, getattr(subject, j)))
|
|
||||||
return ", ".join(bits)
|
|
||||||
|
|
||||||
class CertificateBase:
|
|
||||||
# Others will cause browsers to import the cert instead of offering to
|
|
||||||
# download it
|
|
||||||
content_type = "application/x-pem-file"
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return self.buf
|
|
||||||
|
|
||||||
@property
|
|
||||||
def common_name(self):
|
|
||||||
return self.subject.CN
|
|
||||||
|
|
||||||
@common_name.setter
|
|
||||||
def common_name(self, value):
|
|
||||||
self.subject.CN = value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def key_usage(self):
|
|
||||||
def iterate():
|
|
||||||
for key, value, data in self.extensions:
|
|
||||||
if key == "keyUsage" or key == "extendedKeyUsage":
|
|
||||||
for bit in value.split(", "):
|
|
||||||
if bit == "1.3.6.1.5.5.8.2.2":
|
|
||||||
yield "IKE Intermediate"
|
|
||||||
else:
|
|
||||||
yield bit
|
|
||||||
return ", ".join(iterate())
|
|
||||||
|
|
||||||
@property
|
|
||||||
def subject(self):
|
|
||||||
return self._obj.get_subject()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def issuer(self):
|
|
||||||
return self._obj.get_issuer()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def issuer_dn(self):
|
|
||||||
return subject2dn(self.issuer)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def identity(self):
|
|
||||||
return subject2dn(self.subject)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def key_length(self):
|
|
||||||
return self._obj.get_pubkey().bits()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def key_type(self):
|
|
||||||
if self._obj.get_pubkey().type() == 6:
|
|
||||||
return "RSA"
|
|
||||||
else:
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def extensions(self):
|
|
||||||
for e in self._obj.get_extensions():
|
|
||||||
yield e.get_short_name().decode("ascii"), str(e), e.get_data()
|
|
||||||
|
|
||||||
def set_extensions(self, extensions):
|
|
||||||
# X509Req().add_extensions() first invocation takes only effect?!
|
|
||||||
assert self._obj.get_extensions() == [], "Extensions already set!"
|
|
||||||
self._obj.add_extensions([
|
|
||||||
crypto.X509Extension(
|
|
||||||
key.encode("ascii"),
|
|
||||||
critical,
|
|
||||||
value.encode("ascii")) for (key,value,critical) in extensions])
|
|
||||||
|
|
||||||
@property
|
|
||||||
def fqdn(self):
|
|
||||||
for bit in self.subject_alt_name.split(", "):
|
|
||||||
if bit.startswith("DNS:"):
|
|
||||||
return bit[4:]
|
|
||||||
return ""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def pubkey(self):
|
|
||||||
from Crypto.Util import asn1
|
|
||||||
pubkey_asn1=crypto.dump_privatekey(crypto.FILETYPE_ASN1, self._obj.get_pubkey())
|
|
||||||
pubkey_der=asn1.DerSequence()
|
|
||||||
pubkey_der.decode(pubkey_asn1)
|
|
||||||
zero, modulo, exponent = pubkey_der
|
|
||||||
return modulo, exponent
|
|
||||||
|
|
||||||
@property
|
|
||||||
def pubkey_hex(self):
|
|
||||||
modulo, exponent = self.pubkey
|
|
||||||
h = "%x" % modulo
|
|
||||||
assert len(h) * 4 == self.key_length, "%s is not %s" % (len(h)*4, self.key_length)
|
|
||||||
return re.findall("\d\d", h)
|
|
||||||
|
|
||||||
def fingerprint(self, algorithm="sha256"):
|
|
||||||
return hashlib.new(algorithm, self.buf.encode("ascii")).hexdigest()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def md5sum(self):
|
|
||||||
return self.fingerprint("md5")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def sha1sum(self):
|
|
||||||
return self.fingerprint("sha1")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def sha256sum(self):
|
|
||||||
return self.fingerprint("sha256")
|
|
||||||
|
|
||||||
|
|
||||||
class Request(CertificateBase):
|
|
||||||
|
|
||||||
@property
|
|
||||||
def suggested_filename(self):
|
|
||||||
return self.common_name + ".csr"
|
|
||||||
|
|
||||||
def __init__(self, mixed=None):
|
|
||||||
self.buf = None
|
|
||||||
self.path = NotImplemented
|
|
||||||
self.created = NotImplemented
|
|
||||||
|
|
||||||
if hasattr(mixed, "read"):
|
|
||||||
self.path = mixed.name
|
|
||||||
_, _, _, _, _, _, _, _, mtime, _ = os.stat(self.path)
|
|
||||||
self.created = datetime.fromtimestamp(mtime)
|
|
||||||
mixed = mixed.read()
|
|
||||||
if isinstance(mixed, str):
|
|
||||||
try:
|
|
||||||
self.buf = mixed
|
|
||||||
mixed = crypto.load_certificate_request(crypto.FILETYPE_PEM, mixed)
|
|
||||||
except crypto.Error:
|
|
||||||
raise ValueError("Failed to parse: %s" % mixed)
|
|
||||||
if isinstance(mixed, crypto.X509Req):
|
|
||||||
self._obj = mixed
|
|
||||||
else:
|
|
||||||
raise ValueError("Can't parse %s (%s) as X.509 certificate signing request!" % (mixed, type(mixed)))
|
|
||||||
|
|
||||||
assert not self.buf or self.buf == self.dump(), "%s is not %s" % (repr(self.buf), repr(self.dump()))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_server(self):
|
|
||||||
return "." in self.common_name
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_client(self):
|
|
||||||
return not self.is_server
|
|
||||||
|
|
||||||
def dump(self):
|
|
||||||
return crypto.dump_certificate_request(crypto.FILETYPE_PEM, self._obj).decode("ascii")
|
|
||||||
|
|
||||||
def create(self):
|
|
||||||
# Generate 4096-bit RSA key
|
|
||||||
key = crypto.PKey()
|
|
||||||
key.generate_key(crypto.TYPE_RSA, 4096)
|
|
||||||
|
|
||||||
# Create request
|
|
||||||
req = crypto.X509Req()
|
|
||||||
req.set_pubkey(key)
|
|
||||||
return Request(req)
|
|
||||||
|
|
||||||
|
|
||||||
class Certificate(CertificateBase):
|
|
||||||
|
|
||||||
@property
|
|
||||||
def suggested_filename(self):
|
|
||||||
return self.common_name + ".crt"
|
|
||||||
|
|
||||||
def __init__(self, mixed):
|
|
||||||
self.buf = NotImplemented
|
|
||||||
self.path = NotImplemented
|
|
||||||
self.changed = NotImplemented
|
|
||||||
|
|
||||||
if hasattr(mixed, "read"):
|
|
||||||
self.path = mixed.name
|
|
||||||
_, _, _, _, _, _, _, _, mtime, _ = os.stat(self.path)
|
|
||||||
self.changed = datetime.fromtimestamp(mtime)
|
|
||||||
mixed = mixed.read()
|
|
||||||
if isinstance(mixed, str):
|
|
||||||
try:
|
|
||||||
self.buf = mixed
|
|
||||||
mixed = crypto.load_certificate(crypto.FILETYPE_PEM, mixed)
|
|
||||||
except crypto.Error:
|
|
||||||
raise ValueError("Failed to parse: %s" % mixed)
|
|
||||||
if isinstance(mixed, crypto.X509):
|
|
||||||
self._obj = mixed
|
|
||||||
else:
|
|
||||||
raise ValueError("Can't parse %s (%s) as X.509 certificate!" % (mixed, type(mixed)))
|
|
||||||
|
|
||||||
assert not self.buf or self.buf == self.dump(), "%s is not %s" % (self.buf, self.dump())
|
|
||||||
|
|
||||||
@property
|
|
||||||
def extensions(self):
|
|
||||||
# WTF?!
|
|
||||||
for j in range(0, self._obj.get_extension_count()):
|
|
||||||
e = self._obj.get_extension(j)
|
|
||||||
yield e.get_short_name().decode("ascii"), str(e), e.get_data()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def serial_number(self):
|
|
||||||
return "%040x" % self._obj.get_serial_number()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def serial_number_hex(self):
|
|
||||||
return ":".join(re.findall("[0123456789abcdef]{2}", self.serial_number))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def signed(self):
|
|
||||||
return datetime.strptime(self._obj.get_notBefore().decode("ascii"), "%Y%m%d%H%M%SZ")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def expires(self):
|
|
||||||
return datetime.strptime(self._obj.get_notAfter().decode("ascii"), "%Y%m%d%H%M%SZ")
|
|
||||||
|
|
||||||
def dump(self):
|
|
||||||
return crypto.dump_certificate(crypto.FILETYPE_PEM, self._obj).decode("ascii")
|
|
||||||
|
|
||||||
def digest(self):
|
|
||||||
return self._obj.digest("md5").decode("ascii")
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return self.serial_number == other.serial_number
|
|
||||||
|
|
||||||
def __gt__(self, other):
|
|
||||||
return self.signed > other.signed
|
|
||||||
|
|
||||||
def __lt__(self, other):
|
|
||||||
return self.signed < other.signed
|
|
||||||
|
|
||||||
def __gte__(self, other):
|
|
||||||
return self.signed >= other.signed
|
|
||||||
|
|
||||||
def __lte__(self, other):
|
|
||||||
return self.signed <= other.signed
|
|
||||||
|
|
@ -4,7 +4,7 @@ cryptography==1.7.2
|
|||||||
falcon==1.1.0
|
falcon==1.1.0
|
||||||
humanize==0.5.1
|
humanize==0.5.1
|
||||||
ipaddress==1.0.18
|
ipaddress==1.0.18
|
||||||
Jinja2==2.8
|
Jinja2==2.9.5
|
||||||
Markdown==2.6.8
|
Markdown==2.6.8
|
||||||
pyldap==2.4.28
|
pyldap==2.4.28
|
||||||
requests==2.10.0
|
requests==2.10.0
|
||||||
|
Loading…
Reference in New Issue
Block a user