mirror of
				https://github.com/laurivosandi/certidude
				synced 2025-10-31 01:19:11 +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:
		
							
								
								
									
										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. | ||||
| * `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. | ||||
| * Deep mailbox integration, eg fetch CSR-s from mailbox via IMAP. | ||||
| * `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. | ||||
| * WebCrypto support, meanwhile check out `hwcrypto.js <https://github.com/open-eid/hwcrypto.js>`_. | ||||
| * Certificate push/pull, making it possible to sign offline. | ||||
| * 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 | ||||
| * Ability to send OpenVPN profile URL tokens via e-mail, for simplified VPN adoption. | ||||
| * 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 | ||||
| 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 | ||||
| * Common name is set to short hostname/machine name in AD | ||||
| * E-mail is not filled in (maybe we can fill in something from AD?) | ||||
| * Given name and surname are not filled in | ||||
| * If Kerberos credentials are presented machine can be automatically enrolled depending on the ``machine enrollment`` setting | ||||
| * Common name is set to short ``hostname`` | ||||
| * It is tricky to determine user who is triggering the action so given name, surname and e-mail attributes are not filled in | ||||
|  | ||||
| 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 | ||||
| * Given name and surname are filled in based on LDAP attributes of the user | ||||
| * E-mail not filled in (should it be filled in? Can we even send mail to user if it's in external domain?) | ||||
| * Common name is either set to ``username`` or ``username@device-identifier`` depending on the ``user enrollment`` setting | ||||
| * Given name and surname are not filled in because Unicode characters cause issues in OpenVPN Connect app | ||||
| * E-mail is not filled in because it might change in AD | ||||
|   | ||||
| @@ -5,13 +5,14 @@ import mimetypes | ||||
| import logging | ||||
| import os | ||||
| import click | ||||
| import hashlib | ||||
| from datetime import datetime | ||||
| from time import sleep | ||||
| from certidude import authority, mailer | ||||
| from certidude.auth import login_required, authorize_admin | ||||
| from certidude.user import User | ||||
| 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 | ||||
|  | ||||
| logger = logging.getLogger("api") | ||||
| @@ -44,6 +45,33 @@ class SessionResource(object): | ||||
|     @login_required | ||||
|     @event_source | ||||
|     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( | ||||
|             user = dict( | ||||
|                 name=req.context.get("user").name, | ||||
| @@ -51,29 +79,31 @@ class SessionResource(object): | ||||
|                 sn=req.context.get("user").surname, | ||||
|                 mail=req.context.get("user").mail | ||||
|             ), | ||||
|             request_submission_allowed = sum( # Dirty hack! | ||||
|                 [req.context.get("remote_addr") in j | ||||
|                     for j in config.REQUEST_SUBNETS]), | ||||
|             request_submission_allowed = config.REQUEST_SUBMISSION_ALLOWED, | ||||
|             authority = dict( | ||||
|                 common_name = authority.ca_cert.subject.get_attributes_for_oid( | ||||
|                     NameOID.COMMON_NAME)[0].value, | ||||
|                 outbox = dict( | ||||
|                     server = config.OUTBOX, | ||||
|                     name = config.OUTBOX_NAME, | ||||
|                     mail = config.OUTBOX_MAIL | ||||
|                 ), | ||||
|                 user_certificate_enrollment=config.USER_CERTIFICATE_ENROLLMENT, | ||||
|                 user_mutliple_certificates=config.USER_MULTIPLE_CERTIFICATES, | ||||
|                 certificate = authority.certificate, | ||||
|                 machine_enrollment_allowed=config.MACHINE_ENROLLMENT_ALLOWED, | ||||
|                 user_enrollment_allowed=config.USER_ENROLLMENT_ALLOWED, | ||||
|                 user_multiple_certificates=config.USER_MULTIPLE_CERTIFICATES, | ||||
|                 events = config.EVENT_SOURCE_SUBSCRIBE % config.EVENT_SOURCE_TOKEN, | ||||
|                 requests=authority.list_requests(), | ||||
|                 signed=authority.list_signed(), | ||||
|                 revoked=authority.list_revoked(), | ||||
|                 requests=serialize_requests(authority.list_requests), | ||||
|                 signed=serialize_certificates(authority.list_signed), | ||||
|                 revoked=serialize_certificates(authority.list_revoked), | ||||
|                 users=User.objects.all(), | ||||
|                 admin_users = User.objects.filter_admins(), | ||||
|                 user_subnets = config.USER_SUBNETS, | ||||
|                 autosign_subnets = config.AUTOSIGN_SUBNETS, | ||||
|                 request_subnets = config.REQUEST_SUBNETS, | ||||
|                 admin_subnets=config.ADMIN_SUBNETS, | ||||
|                 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 | ||||
|                 ) | ||||
|             ) if req.context.get("user").is_admin() else None, | ||||
| @@ -88,7 +118,6 @@ class StaticResource(object): | ||||
|         self.root = os.path.realpath(root) | ||||
|  | ||||
|     def __call__(self, req, resp): | ||||
|  | ||||
|         path = os.path.realpath(os.path.join(self.root, req.path[1:])) | ||||
|         if not path.startswith(self.root): | ||||
|             raise falcon.HTTPForbidden | ||||
| @@ -124,7 +153,7 @@ def certidude_app(): | ||||
|     from certidude import config | ||||
|     from .bundle import BundleResource | ||||
|     from .revoked import RevocationListResource | ||||
|     from .signed import SignedCertificateListResource, SignedCertificateDetailResource | ||||
|     from .signed import SignedCertificateDetailResource | ||||
|     from .request import RequestListResource, RequestDetailResource | ||||
|     from .lease import LeaseResource, StatusFileLeaseResource | ||||
|     from .whois import WhoisResource | ||||
| @@ -138,7 +167,6 @@ def certidude_app(): | ||||
|     app.add_route("/api/certificate/", CertificateAuthorityResource()) | ||||
|     app.add_route("/api/revoked/", RevocationListResource()) | ||||
|     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/", RequestListResource()) | ||||
|     app.add_route("/api/", SessionResource()) | ||||
| @@ -151,7 +179,7 @@ def certidude_app(): | ||||
|         app.add_route("/api/whois/", WhoisResource()) | ||||
|  | ||||
|     # Optional user enrollment API call | ||||
|     if config.USER_CERTIFICATE_ENROLLMENT: | ||||
|     if config.USER_ENROLLMENT_ALLOWED: | ||||
|         app.add_route("/api/bundle/", BundleResource()) | ||||
|  | ||||
|     if config.TAGGING_BACKEND == "sql": | ||||
|   | ||||
| @@ -1,5 +1,3 @@ | ||||
|  | ||||
|  | ||||
| import logging | ||||
| import hashlib | ||||
| from certidude import config, authority | ||||
|   | ||||
| @@ -4,91 +4,125 @@ import falcon | ||||
| import logging | ||||
| import ipaddress | ||||
| import os | ||||
| import hashlib | ||||
| from base64 import b64decode | ||||
| from certidude import config, authority, helpers, push, errors | ||||
| from certidude.auth import login_required, login_optional, authorize_admin | ||||
| from certidude.decorators import serialize, csrf_protection | ||||
| from certidude.wrappers import Request, Certificate | ||||
| from certidude.firewall import whitelist_subnets, whitelist_content_types | ||||
|  | ||||
| from cryptography import x509 | ||||
| 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") | ||||
|  | ||||
| class RequestListResource(object): | ||||
|     @serialize | ||||
|     @login_required | ||||
|     @authorize_admin | ||||
|     def on_get(self, req, resp): | ||||
|         return authority.list_requests() | ||||
|  | ||||
|  | ||||
|     @login_optional | ||||
|     @whitelist_subnets(config.REQUEST_SUBNETS) | ||||
|     @whitelist_content_types("application/pkcs10") | ||||
|     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) | ||||
|  | ||||
|         # Normalize body, TODO: newlines | ||||
|         if not body.endswith("\n"): | ||||
|             body += "\n" | ||||
|  | ||||
|         csr = Request(body) | ||||
|  | ||||
|         if not csr.common_name: | ||||
|         csr = x509.load_pem_x509_csr(body, default_backend()) | ||||
|         try: | ||||
|             common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME) | ||||
|         except: # ValueError? | ||||
|             logger.warning(u"Rejected signing request without common name from %s", | ||||
|                 req.context.get("remote_addr")) | ||||
|             raise falcon.HTTPBadRequest( | ||||
|                 "Bad request", | ||||
|                 "No common name specified!") | ||||
|  | ||||
|         """ | ||||
|         Handle domain computer automatic enrollment | ||||
|         """ | ||||
|         machine = req.context.get("machine") | ||||
|         if machine: | ||||
|             if csr.common_name != machine: | ||||
|         if config.MACHINE_ENROLLMENT_ALLOWED and machine: | ||||
|             if common_name.value != machine: | ||||
|                 raise falcon.HTTPBadRequest( | ||||
|                     "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 | ||||
|             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 | ||||
|  | ||||
|  | ||||
|         # 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: | ||||
|             cert = authority.get_signed(csr.common_name) | ||||
|             path, buf, cert = authority.get_signed(common_name.value) | ||||
|         except EnvironmentError: | ||||
|             pass | ||||
|         else: | ||||
|             if cert.pubkey == csr.pubkey: | ||||
|             if cert.public_key().public_numbers() == csr.public_key().public_numbers(): | ||||
|                 try: | ||||
|                     renewal_signature = b64decode(req.get_header("X-Renewal-Signature")) | ||||
|                 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", csr.common_name) | ||||
|                     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: | ||||
|                 if req.context.get("remote_addr") in subnet: | ||||
|                     try: | ||||
|                         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 | ||||
|                     except EnvironmentError: # Certificate already exists, try to save the request | ||||
|                         pass | ||||
|                     except EnvironmentError: | ||||
|                         logger.info("Autosign for %s failed, signed certificate already exists", | ||||
|                             common_name.value, req.context.get("remote_addr")) | ||||
|                     break | ||||
|  | ||||
|         # Attempt to save the request otherwise | ||||
|         try: | ||||
|             csr = authority.store_request(body) | ||||
|         except errors.RequestExists: | ||||
|             # We should stil redirect client to long poll URL below | ||||
|             # We should still redirect client to long poll URL below | ||||
|             pass | ||||
|         except errors.DuplicateCommonNameError: | ||||
|             # TODO: Certificate renewal | ||||
| @@ -98,12 +132,13 @@ class RequestListResource(object): | ||||
|                 "CSR with such CN already exists", | ||||
|                 "Will not overwrite existing certificate signing request, explicitly delete CSR and try again") | ||||
|         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 | ||||
|         logger.info(u"Signing request %s from %s stored", common_name.value, req.context.get("remote_addr")) | ||||
|         if req.get_param("wait"): | ||||
|             # 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) | ||||
|             resp.status = falcon.HTTP_SEE_OTHER | ||||
|             resp.set_header("Location", url.encode("ascii")) | ||||
| @@ -111,20 +146,17 @@ class RequestListResource(object): | ||||
|         else: | ||||
|             # Request was accepted, but not processed | ||||
|             resp.status = falcon.HTTP_202 | ||||
|             logger.info(u"Signing request from %s stored", req.context.get("remote_addr")) | ||||
|  | ||||
|  | ||||
| class RequestDetailResource(object): | ||||
|     @serialize | ||||
|     def on_get(self, req, resp, cn): | ||||
|         """ | ||||
|         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", | ||||
|             csr.common_name, req.context.get("remote_addr")) | ||||
|         return csr | ||||
|  | ||||
|             cn, req.context.get("remote_addr")) | ||||
|  | ||||
|     @csrf_protection | ||||
|     @login_required | ||||
| @@ -133,16 +165,15 @@ class RequestDetailResource(object): | ||||
|         """ | ||||
|         Sign a certificate signing request | ||||
|         """ | ||||
|         csr = authority.get_request(cn) | ||||
|         cert = authority.sign(csr, overwrite=True, delete=True) | ||||
|         os.unlink(csr.path) | ||||
|         cert, buf = authority.sign(cn, overwrite=True) | ||||
|         # Mailing and long poll publishing implemented in the function above | ||||
|  | ||||
|         resp.body = "Certificate successfully signed" | ||||
|         resp.status = falcon.HTTP_201 | ||||
|         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")) | ||||
|  | ||||
|  | ||||
|     @csrf_protection | ||||
|     @login_required | ||||
|     @authorize_admin | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
|  | ||||
| import click | ||||
| import falcon | ||||
| import json | ||||
| import logging | ||||
| from certidude import const, config | ||||
| from certidude.authority import export_crl, list_revoked | ||||
| from certidude.decorators import MyEncoder | ||||
| from cryptography import x509 | ||||
| from cryptography.hazmat.backends import default_backend | ||||
| from cryptography.hazmat.primitives.serialization import Encoding | ||||
| @@ -31,16 +31,13 @@ class RevocationListResource(object): | ||||
|                 resp.status = falcon.HTTP_SEE_OTHER | ||||
|                 resp.set_header("Location", url.encode("ascii")) | ||||
|                 logger.debug(u"Redirecting to CRL request to %s", url) | ||||
|                 resp.body = "Redirecting to %s" % url | ||||
|             else: | ||||
|                 resp.set_header("Content-Type", "application/x-pem-file") | ||||
|                 resp.append_header( | ||||
|                     "Content-Disposition", | ||||
|                     ("attachment; filename=%s-crl.pem" % const.HOSTNAME).encode("ascii")) | ||||
|                 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: | ||||
|             raise falcon.HTTPUnsupportedMediaType( | ||||
|                 "Client did not accept application/x-pkcs7-crl or application/x-pem-file") | ||||
|   | ||||
| @@ -1,38 +1,46 @@ | ||||
|  | ||||
| import falcon | ||||
| import logging | ||||
| import json | ||||
| import hashlib | ||||
| from certidude import authority | ||||
| 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") | ||||
|  | ||||
| class SignedCertificateListResource(object): | ||||
|     @serialize | ||||
|     @login_required | ||||
|     @authorize_admin | ||||
|     def on_get(self, req, resp): | ||||
|         return {"signed":authority.list_signed()} | ||||
|  | ||||
|  | ||||
| class SignedCertificateDetailResource(object): | ||||
|     @serialize | ||||
|     def on_get(self, req, resp, cn): | ||||
|         # Compensate for NTP lag | ||||
| #        from time import sleep | ||||
| #        sleep(5) | ||||
|  | ||||
|         preferred_type = req.client_prefers(("application/json", "application/x-pem-file")) | ||||
|         try: | ||||
|             cert = authority.get_signed(cn) | ||||
|             path, buf, cert = authority.get_signed(cn) | ||||
|         except EnvironmentError: | ||||
|             logger.warning(u"Failed to serve non-existant certificate %s to %s", | ||||
|                 cn, req.context.get("remote_addr")) | ||||
|             resp.body = "No certificate CN=%s found" % cn | ||||
|             raise falcon.HTTPNotFound() | ||||
|             raise falcon.HTTPNotFound("No certificate CN=%s found" % cn) | ||||
|         else: | ||||
|             logger.debug(u"Served certificate %s to %s", | ||||
|             if preferred_type == "application/x-pem-file": | ||||
|                 resp.set_header("Content-Type", "application/x-pem-file") | ||||
|                 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")) | ||||
|             return cert | ||||
|  | ||||
|             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 | ||||
|     @login_required | ||||
| @@ -40,5 +48,5 @@ class SignedCertificateDetailResource(object): | ||||
|     def on_delete(self, req, resp, cn): | ||||
|         logger.info(u"Revoked certificate %s by %s from %s", | ||||
|             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: | ||||
|         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 wrapper(func): | ||||
| @@ -38,7 +40,7 @@ def authenticate(optional=False): | ||||
|             # If LDAP enabled and device is not Kerberos capable fall | ||||
|             # back to LDAP bind authentication | ||||
|             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) | ||||
|  | ||||
|             # Try pre-emptive authentication | ||||
| @@ -81,16 +83,20 @@ def authenticate(optional=False): | ||||
|                 raise falcon.HTTPForbidden("Forbidden", | ||||
|                     "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 | ||||
|                 # TODO: Assert LDAP group membership | ||||
|                 req.context["machine"], _ = user.lower().split("$@", 1) | ||||
|                 req.context["machine"] = username[:-1].lower() | ||||
|                 req.context["user"] = None | ||||
|             else: | ||||
|                 # Attempt to look up real user | ||||
|                 req.context["user"] = User.objects.get(user) | ||||
|                 req.context["user"] = User.objects.get(username) | ||||
|  | ||||
|             try: | ||||
|                 kerberos.authGSSServerClean(context) | ||||
| @@ -143,12 +149,8 @@ def authenticate(optional=False): | ||||
|             conn = ldap.initialize(config.LDAP_AUTHENTICATION_URI) | ||||
|             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: | ||||
|                 conn.simple_bind_s(user, passwd) | ||||
|                 conn.simple_bind_s("%s@%s" % (user, const.DOMAIN), passwd) | ||||
|             except ldap.STRONG_AUTH_REQUIRED: | ||||
|                 logger.critical("LDAP server demands encryption, use ldaps:// instead of ldaps://") | ||||
|                 raise | ||||
| @@ -160,7 +162,7 @@ def authenticate(optional=False): | ||||
|                 logger.critical(u"LDAP bind authentication failed for user %s from  %s", | ||||
|                     repr(user), req.context.get("remote_addr")) | ||||
|                 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",)) | ||||
|  | ||||
|             req.context["ldap_conn"] = conn | ||||
|   | ||||
| @@ -4,15 +4,15 @@ import os | ||||
| import random | ||||
| import re | ||||
| import requests | ||||
| import hashlib | ||||
| import socket | ||||
| from datetime import datetime, timedelta | ||||
| from cryptography.hazmat.backends import default_backend | ||||
| 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 import hashes, serialization | ||||
| from certidude import config, push, mailer, const | ||||
| from certidude.wrappers import Certificate, Request | ||||
| from certidude import errors | ||||
| 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 | ||||
|  | ||||
| # 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): | ||||
|     if not re.match(RE_HOSTNAME, 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): | ||||
|     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(common_name): | ||||
|     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"))) | ||||
|     path = os.path.join(config.SIGNED_DIR, common_name + ".pem") | ||||
|     with open(path) as fh: | ||||
|         buf = fh.read() | ||||
|         return path, buf, x509.load_pem_x509_certificate(buf, default_backend()) | ||||
|  | ||||
| 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): | ||||
|     """ | ||||
|     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()) | ||||
|     for name in csr.subject: | ||||
|         if name.oid == NameOID.COMMON_NAME: | ||||
|             common_name = name.value | ||||
|             break | ||||
|     else: | ||||
|         raise ValueError("No common name in %s" % csr.subject) | ||||
|     common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME) | ||||
|     # TODO: validate common name again | ||||
|  | ||||
|     request_path = os.path.join(config.REQUESTS_DIR, common_name + ".pem") | ||||
|  | ||||
|     if not re.match(RE_HOSTNAME, common_name): | ||||
|     if not re.match(RE_HOSTNAME, common_name.value): | ||||
|         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 os.path.exists(request_path): | ||||
|         if open(request_path).read() == buf: | ||||
| @@ -99,9 +79,11 @@ def store_request(buf, overwrite=False): | ||||
|             fh.write(buf) | ||||
|         os.rename(request_path + ".part", request_path) | ||||
|  | ||||
|     req = Request(open(request_path)) | ||||
|     mailer.send("request-stored.md", attachments=(req,), request=req) | ||||
|     return req | ||||
|     attach_csr = buf, "application/x-pem-file", common_name.value + ".csr" | ||||
|     mailer.send("request-stored.md", | ||||
|         attachments=(attach_csr,), | ||||
|         common_name=common_name.value) | ||||
|     return csr | ||||
|  | ||||
|  | ||||
| def signer_exec(cmd, *bits): | ||||
| @@ -118,14 +100,15 @@ def signer_exec(cmd, *bits): | ||||
|     return buf | ||||
|  | ||||
|  | ||||
| def revoke_certificate(common_name): | ||||
| def revoke(common_name): | ||||
|     """ | ||||
|     Revoke valid certificate | ||||
|     """ | ||||
|     cert = get_signed(common_name) | ||||
|     revoked_filename = os.path.join(config.REVOKED_DIR, "%s.pem" % cert.serial_number) | ||||
|     os.rename(cert.path, revoked_filename) | ||||
|     push.publish("certificate-revoked", cert.common_name) | ||||
|     path, buf, cert = get_signed(common_name) | ||||
|     revoked_path = os.path.join(config.REVOKED_DIR, "%x.pem" % cert.serial_number) | ||||
|     signed_path = os.path.join(config.SIGNED_DIR, "%s.pem" % common_name) | ||||
|     os.rename(signed_path, revoked_path) | ||||
|     push.publish("certificate-revoked", common_name) | ||||
|  | ||||
|     # Publish CRL for long polls | ||||
|     if config.LONG_POLL_PUBLISH: | ||||
| @@ -134,26 +117,52 @@ def revoke_certificate(common_name): | ||||
|         requests.post(url, data=export_crl(), | ||||
|             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): | ||||
|     for filename in os.listdir(directory): | ||||
|         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_signed(directory=config.SIGNED_DIR): | ||||
| def _list_certificates(directory): | ||||
|     for filename in os.listdir(directory): | ||||
|         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): | ||||
|     for filename in os.listdir(directory): | ||||
|         if filename.endswith(".pem"): | ||||
|             yield Certificate(open(os.path.join(directory, filename))) | ||||
|  | ||||
| def list_revoked(): | ||||
|     return _list_certificates(config.REVOKED_DIR) | ||||
|  | ||||
| def export_crl(): | ||||
|     sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | ||||
| @@ -178,14 +187,15 @@ def delete_request(common_name): | ||||
|         raise ValueError("Invalid common name") | ||||
|  | ||||
|     path = os.path.join(config.REQUESTS_DIR, common_name + ".pem") | ||||
|     request = Request(open(path)) | ||||
|     _, buf, csr = get_request(common_name) | ||||
|     os.unlink(path) | ||||
|  | ||||
|     # 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 | ||||
|     requests.delete(config.LONG_POLL_PUBLISH % request.fingerprint(), | ||||
|     requests.delete( | ||||
|         config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest(), | ||||
|         headers={"User-Agent": "Certidude API"}) | ||||
|  | ||||
| def generate_ovpn_bundle(common_name, owner=None): | ||||
| @@ -198,26 +208,26 @@ def generate_ovpn_bundle(common_name, owner=None): | ||||
|         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([ | ||||
|         x509.NameAttribute(k, v) for k, v in ( | ||||
|             (NameOID.COMMON_NAME, common_name), | ||||
|         ) if v | ||||
|     ])) | ||||
|     ])).sign(key, hashes.SHA512(), default_backend()) | ||||
|  | ||||
|     buf = csr.public_bytes(serialization.Encoding.PEM) | ||||
|  | ||||
|     # Sign CSR | ||||
|     cert = sign(Request( | ||||
|         csr.sign(key, hashes.SHA512(), default_backend()).public_bytes(serialization.Encoding.PEM)), overwrite=True) | ||||
|     cert, cert_buf = _sign(csr, buf, overwrite=True) | ||||
|  | ||||
|     bundle = Template(open(config.OPENVPN_BUNDLE_TEMPLATE).read()).render( | ||||
|         ca = certificate.dump(), | ||||
|         key = key.private_bytes( | ||||
|             encoding=serialization.Encoding.PEM, | ||||
|             format=serialization.PrivateFormat.TraditionalOpenSSL, | ||||
|             encryption_algorithm=serialization.NoEncryption() | ||||
|         ), | ||||
|         cert = cert.dump(), | ||||
|         crl=export_crl(), | ||||
|     ) | ||||
|     bundle = Template(open(config.OPENVPN_PROFILE_TEMPLATE).read()).render( | ||||
|         ca = ca_buf, key = key_buf, cert = cert_buf, crl=export_crl(), | ||||
|         servers = [cn for cn, path, buf, cert, server in list_signed() if server]) | ||||
|     return bundle, cert | ||||
|  | ||||
| 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([ | ||||
|         x509.NameAttribute(NameOID.COMMON_NAME, common_name) | ||||
|     ])) | ||||
|     ])).sign(key, hashes.SHA512(), default_backend()) | ||||
|  | ||||
|     buf = csr.public_bytes(serialization.Encoding.PEM) | ||||
|  | ||||
|     # Sign CSR | ||||
|     cert = sign(Request( | ||||
|         csr.sign(key, hashes.SHA512(), default_backend()).public_bytes(serialization.Encoding.PEM)), overwrite=True) | ||||
|     cert, cert_buf = _sign(csr, buf, overwrite=True) | ||||
|  | ||||
|     # Generate P12, currently supported only by PyOpenSSL | ||||
|     try: | ||||
| @@ -256,131 +267,102 @@ def generate_pkcs12_bundle(common_name, key_size=4096, owner=None): | ||||
|                 key.private_bytes( | ||||
|                         encoding=serialization.Encoding.PEM, | ||||
|                         format=serialization.PrivateFormat.TraditionalOpenSSL, | ||||
|                         encryption_algorithm=serialization.NoEncryption() | ||||
|                     ) | ||||
|                 ) | ||||
|             ) | ||||
|         p12.set_certificate( cert._obj ) | ||||
|         p12.set_ca_certificates([certificate._obj]) | ||||
|                         encryption_algorithm=serialization.NoEncryption()))) | ||||
|         p12.set_certificate( | ||||
|             crypto.load_certificate(crypto.FILETYPE_PEM, cert_buf)) | ||||
|         p12.set_ca_certificates([ | ||||
|             crypto.load_certificate(crypto.FILETYPE_PEM, ca_buf)]) | ||||
|         return p12.export("1234"), cert | ||||
|  | ||||
|  | ||||
| @publish_certificate | ||||
| def sign(req, overwrite=False, delete=True): | ||||
| def sign(common_name, overwrite=False): | ||||
|     """ | ||||
|     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 | ||||
|     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: | ||||
|             revoke_certificate(req.common_name) | ||||
|         elif req.pubkey == old_cert.pubkey: | ||||
|             return old_cert | ||||
|             if renew: | ||||
|                 # TODO: is this the best approach? | ||||
|                 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: | ||||
|             raise EnvironmentError("Will not overwrite existing certificate") | ||||
|  | ||||
|     # 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: | ||||
|         fh.write(cert_buf) | ||||
|     os.rename(cert_path + ".part", cert_path) | ||||
|  | ||||
|     return Certificate(open(cert_path)) | ||||
|     # Send mail | ||||
|     recipient = None | ||||
|  | ||||
|  | ||||
| @publish_certificate | ||||
| def sign2(request, private_key, authority_certificate, overwrite=False, delete=True, lifetime=None): | ||||
|     """ | ||||
|     Sign directly using private key, this is usually done by root. | ||||
|     Basic constraints and certificate lifetime are copied from config, | ||||
|     lifetime may be overridden on the command line, | ||||
|     other extensions are copied as is. | ||||
|     """ | ||||
|  | ||||
|     certificate_path = os.path.join(config.SIGNED_DIR, request.common_name + ".pem") | ||||
|     if os.path.exists(certificate_path): | ||||
|         if overwrite: | ||||
|             revoke_certificate(request.common_name) | ||||
|     if renew: | ||||
|         mailer.send( | ||||
|             "certificate-renewed.md", | ||||
|             to=recipient, | ||||
|             attachments=( | ||||
|                 (prev_buf, "application/x-pem-file", "deprecated.crt"), | ||||
|                 (cert_buf, "application/x-pem-file", common_name.value + ".crt") | ||||
|             ), | ||||
|             serial_number="%x" % cert.serial, | ||||
|             common_name=common_name.value, | ||||
|             certificate=cert, | ||||
|         ) | ||||
|     else: | ||||
|             raise errors.DuplicateCommonNameError("Valid certificate with common name %s already exists" % request.common_name) | ||||
|  | ||||
|     now = datetime.utcnow() | ||||
|     request_path = os.path.join(config.REQUESTS_DIR, request.common_name + ".pem") | ||||
|     request = x509.load_pem_x509_csr(open(request_path).read(), default_backend()) | ||||
|  | ||||
|     cert = x509.CertificateBuilder( | ||||
|         ).subject_name(x509.Name([n for n in request.subject]) | ||||
|         ).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 | ||||
|         mailer.send( | ||||
|             "certificate-signed.md", | ||||
|             to=recipient, | ||||
|             attachments=( | ||||
|                 (buf,      "application/x-pem-file", common_name.value + ".csr"), | ||||
|                 (cert_buf, "application/x-pem-file", common_name.value + ".crt") | ||||
|             ), | ||||
|             serial_number="%x" % cert.serial, | ||||
|             common_name=common_name.value, | ||||
|             certificate=cert, | ||||
|         ) | ||||
|  | ||||
|     # Append subject alternative name, extended key usage flags etc | ||||
|     for extension in request.extensions: | ||||
|         if extension.oid == ExtensionOID.SUBJECT_ALTERNATIVE_NAME: | ||||
|             click.echo("Appending subject alt name extension: %s" % extension) | ||||
|             cert = cert.add_extension(x509.SubjectAlternativeName(extension.value), | ||||
|                 critical=extension.critical) | ||||
|         if extension.oid == ExtensionOID.EXTENDED_KEY_USAGE: | ||||
|             click.echo("Appending extended key usage flags extension: %s" % extension) | ||||
|             cert = cert.add_extension(x509.ExtendedKeyUsage(extension.value), | ||||
|                 critical=extension.critical) | ||||
|  | ||||
|     if config.LONG_POLL_PUBLISH: | ||||
|         url = config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest() | ||||
|         click.echo("Publishing certificate at %s ..." % url) | ||||
|         requests.post(url, data=cert_buf, | ||||
|             headers={"User-Agent": "Certidude API", "Content-Type": "application/x-x509-user-cert"}) | ||||
|  | ||||
|     if config.EVENT_SOURCE_PUBLISH: # TODO: handle renewal | ||||
|         push.publish("request-signed", common_name.value) | ||||
|  | ||||
|     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 humanize import naturaltime | ||||
| from jinja2 import Environment, PackageLoader | ||||
| from time import sleep | ||||
| from setproctitle import setproctitle | ||||
| import const | ||||
|  | ||||
| @@ -38,22 +37,29 @@ env = Environment(loader=PackageLoader("certidude", "templates"), trim_blocks=Tr | ||||
|  | ||||
| # Parse command-line argument defaults from environment | ||||
|  | ||||
| USERNAME = os.environ.get("USER") | ||||
| NOW = datetime.utcnow().replace(tzinfo=None) | ||||
| FIRST_NAME = None | ||||
| SURNAME = None | ||||
| EMAIL = None | ||||
|  | ||||
| if USERNAME: | ||||
|     EMAIL = USERNAME + "@" + const.FQDN | ||||
| CERTIDUDE_TIMER = """ | ||||
| [Unit] | ||||
| Description=Run certidude service weekly | ||||
|  | ||||
| if os.getuid() >= 1000: | ||||
|     _, _, _, _, gecos, _, _ = pwd.getpwnam(USERNAME) | ||||
|     if " " in gecos: | ||||
|         FIRST_NAME, SURNAME = gecos.split(" ", 1) | ||||
|     else: | ||||
|         FIRST_NAME = gecos | ||||
| [Timer] | ||||
| OnCalendar=weekly | ||||
| Persistent=true | ||||
| Unit=certidude.service | ||||
|  | ||||
| [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.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) | ||||
|         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(): | ||||
|         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: | ||||
|             endpoint_insecure = clients.getboolean(authority, "insecure") | ||||
|         except NoOptionError: | ||||
| @@ -111,22 +134,6 @@ def certidude_request(fork): | ||||
|             endpoint_revocations_path = "/var/lib/certidude/%s/ca_crl.pem" % authority | ||||
|         # 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 not os.path.exists("/etc/krb5.keytab"): | ||||
|                 continue | ||||
| @@ -168,8 +175,6 @@ def certidude_request(fork): | ||||
|                     endpoint_authority_path, | ||||
|                     endpoint_revocations_path, | ||||
|                     endpoint_common_name, | ||||
|                     extended_key_usage_flags, | ||||
|                     None, | ||||
|                     insecure=endpoint_insecure, | ||||
|                     autosign=True, | ||||
|                     wait=True) | ||||
| @@ -229,6 +234,10 @@ def certidude_request(fork): | ||||
|  | ||||
|             # OpenVPN set up with NetworkManager | ||||
|             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.add_section("connection") | ||||
|                 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", "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", "port", "51900") | ||||
|                 nm_config.set("vpn", "key", endpoint_key_path) | ||||
|                 nm_config.set("vpn", "cert", endpoint_certificate_path) | ||||
|                 nm_config.set("vpn", "ca", endpoint_authority_path) | ||||
| @@ -255,9 +265,9 @@ def certidude_request(fork): | ||||
|                 os.umask(0o177) | ||||
|  | ||||
|                 # 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) | ||||
|                     click.echo("Created %s" % fh.name) | ||||
|                     click.echo("Created %s" % nm_config_path) | ||||
|                 os.system("nmcli con reload") | ||||
|                 continue | ||||
|  | ||||
| @@ -302,50 +312,18 @@ def certidude_request(fork): | ||||
|         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.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("--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("--route", "-r", type=ip_network, multiple=True, help="Subnets to advertise via this connection, multiple allowed") | ||||
| @click.option("--config", "-o", | ||||
|     default="/etc/openvpn/site-to-client.conf", | ||||
|     type=click.File(mode="w", atomic=True, lazy=True), | ||||
|     help="OpenVPN configuration file") | ||||
| def certidude_setup_openvpn_server(authority, config, subnet, route, org_unit, 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 | ||||
| def certidude_setup_openvpn_server(authority, config, subnet, route, local, proto, port): | ||||
|  | ||||
|     # Create corresponding section in Certidude client configuration file | ||||
|     client_config = ConfigParser() | ||||
| @@ -356,13 +334,12 @@ def certidude_setup_openvpn_server(authority, config, subnet, route, org_unit, l | ||||
|     else: | ||||
|         client_config.set(authority, "trigger", "interface up") | ||||
|         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, "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, "authority path",  "/etc/openvpn/keys/ca.crt") | ||||
|         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: | ||||
|             client_config.write(fh) | ||||
|         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.set(endpoint, "authority", authority) | ||||
|         service_config.set(endpoint, "service", "init/openvpn") | ||||
|  | ||||
|         with open(const.SERVICES_CONFIG_PATH + ".part", 'wb') as fh: | ||||
|             service_config.write(fh) | ||||
|         os.rename(const.SERVICES_CONFIG_PATH + ".part", const.SERVICES_CONFIG_PATH) | ||||
|         click.echo("Section '%s' added to %s" % (endpoint, const.SERVICES_CONFIG_PATH)) | ||||
|  | ||||
|     dhparam_path = "/etc/openvpn/keys/dhparam.pem" | ||||
|     if not os.path.exists(dhparam_path): | ||||
|         cmd = "openssl", "dhparam", "-out", dhparam_path, "2048" | ||||
|         subprocess.check_call(cmd) | ||||
|  | ||||
|     config.write("mode server\n") | ||||
|     config.write("tls-server\n") | ||||
|     authority_hostname = authority.split(".")[0] | ||||
|     config.write("server %s %s\n" % (subnet.network_address, subnet.netmask)) | ||||
|     config.write("dev tun-%s\n" % authority_hostname) | ||||
|     config.write("proto %s\n" % proto) | ||||
|     config.write("port %d\n" % port) | ||||
|     config.write("dev tap\n") | ||||
|     config.write("local %s\n" % local) | ||||
|     config.write("key %s\n" % client_config.get(authority, "key 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("dh %s\n" % dhparam_path) | ||||
|     config.write("dh %s\n" % client_config.get(authority, "dhparam path")) | ||||
|     config.write("comp-lzo\n") | ||||
|     config.write("user nobody\n") | ||||
|     config.write("group nogroup\n") | ||||
|     config.write("persist-tun\n") | ||||
|     config.write("persist-key\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("#ifconfig-pool-persist /tmp/openvpn-leases.txt\n") | ||||
|     config.write("#crl-verify %s\n" % client_config.get(authority, "revocations path")) | ||||
|  | ||||
|     click.echo("Generated %s" % config.name) | ||||
|     click.echo("Inspect generated files and issue following to request certificate:") | ||||
|     click.echo() | ||||
|     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.argument("server") | ||||
| @click.argument("authority") | ||||
| @click.option("--common-name", "-cn", default=const.FQDN, help="Common name, %s by default" % const.FQDN) | ||||
| @click.option("--tls-config", | ||||
|     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("--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("--verify-client", "-vc", type=click.Choice(['optional', 'on', 'off'])) | ||||
| @click.option("--verify-client", "-vc", default="optional", type=click.Choice(['optional', 'on', 'off'])) | ||||
| @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): | ||||
|     # TODO: Intelligent way of getting last IP address in the subnet | ||||
|  | ||||
|     if not os.path.exists(certificate_path): | ||||
|         click.echo("As HTTPS 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" % common_name) | ||||
|         click.echo() | ||||
|     retval = certidude_request_certificate(authority, key_path, request_path, | ||||
|         certificate_path, authority_path, revocations_path, common_name, org_unit, | ||||
|         extended_key_usage_flags = [ExtendedKeyUsageOID.SERVER_AUTH], | ||||
|         dns = const.FQDN, wait=True, bundle=True) | ||||
|  | ||||
|     if not os.path.exists(dhparam_path): | ||||
|         cmd = "openssl", "dhparam", "-out", dhparam_path, "2048" | ||||
|         subprocess.check_call(cmd) | ||||
|  | ||||
|     if retval: | ||||
|         return retval | ||||
| 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): | ||||
|     if not os.path.exists("/etc/nginx"): | ||||
|         raise ValueError("nginx not installed") | ||||
|     if "." not in common_name: | ||||
|         raise ValueError("Fully qualified hostname not specified as common name, make sure hostname -f works") | ||||
|     client_config = ConfigParser() | ||||
|     if os.path.exists(const.CLIENT_CONFIG_PATH): | ||||
|         client_config.readfp(open(const.CLIENT_CONFIG_PATH)) | ||||
|     if client_config.has_section(authority): | ||||
|         click.echo("Section '%s' already exists in %s, remove to regenerate" % (authority, const.CLIENT_CONFIG_PATH)) | ||||
|     else: | ||||
|         client_config.add_section(authority) | ||||
|         client_config.set(authority, "trigger", "interface up") | ||||
|         client_config.set(authority, "common name", common_name) | ||||
|         client_config.set(authority, "request path", request_path) | ||||
|         client_config.set(authority, "key path", key_path) | ||||
|         client_config.set(authority, "certificate path", certificate_path) | ||||
|         client_config.set(authority, "authority path",  authority_path) | ||||
|         client_config.set(authority, "dhparam path",  dhparam_path) | ||||
|         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.update(locals()) | ||||
|  | ||||
|     if os.path.exists(site_client_config.name): | ||||
|         click.echo("Configuration file %s already exists, not overwriting" % site_client_config.name) | ||||
|     if os.path.exists(site_config.name): | ||||
|         click.echo("Configuration file %s already exists, not overwriting" % site_config.name) | ||||
|     else: | ||||
|         site_client_config.write(env.get_template("nginx-https-site.conf").render(context)) | ||||
|         click.echo("Generated %s" % site_client_config.name) | ||||
|         site_config.write(env.get_template("nginx-https-site.conf").render(context)) | ||||
|         click.echo("Generated %s" % site_config.name) | ||||
|  | ||||
|     if os.path.exists(tls_client_config.name): | ||||
|         click.echo("Configuration file %s already exists, not overwriting" % tls_client_config.name) | ||||
|     if os.path.exists(tls_config.name): | ||||
|         click.echo("Configuration file %s already exists, not overwriting" % tls_config.name) | ||||
|     else: | ||||
|         tls_client_config.write(env.get_template("nginx-tls.conf").render(context)) | ||||
|         click.echo("Generated %s" % tls_client_config.name) | ||||
|         tls_config.write(env.get_template("nginx-tls.conf").render(context)) | ||||
|         click.echo("Generated %s" % tls_config.name) | ||||
|  | ||||
|  | ||||
|     click.echo() | ||||
|     click.echo("Inspect configuration files, enable it and start nginx service:") | ||||
|     click.echo() | ||||
|     click.echo("  ln -s %s /etc/nginx/sites-enabled/%s" % ( | ||||
|         os.path.relpath(site_client_config.name, "/etc/nginx/sites-enabled"), | ||||
|         os.path.basename(site_client_config.name))) | ||||
|     click.secho("  service nginx restart", bold=True) | ||||
|         os.path.relpath(site_config.name, "/etc/nginx/sites-enabled"), | ||||
|         os.path.basename(site_config.name))) | ||||
|     click.echo("  service nginx restart") | ||||
|     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", | ||||
|     type=click.File(mode="w", atomic=True, lazy=True), | ||||
|     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 | ||||
|     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() | ||||
|     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.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("--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") | ||||
| @@ -580,8 +546,6 @@ def certidude_setup_strongswan_server(authority, config, secrets, subnet, route, | ||||
|     else: | ||||
|         client_config.set(authority, "trigger", "interface up") | ||||
|         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, "key path", "/etc/ipsec.d/private/%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.argument("server") | ||||
| @click.argument("authority") | ||||
| @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 | ||||
|     client_config = ConfigParser() | ||||
|     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.argument("server") # Certidude server | ||||
| @click.argument("authority") # Certidude server | ||||
| @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 | ||||
|  | ||||
|     # 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.set(authority, "trigger", "interface up") | ||||
|         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, "key path", "/etc/ipsec.d/private/%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.argument("server") # Certidude server | ||||
| @click.argument("authority") | ||||
| @click.argument("remote") # OpenVPN gateway | ||||
| @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 | ||||
|     client_config = ConfigParser() | ||||
|     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.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("--nginx-config", "-n", | ||||
|     default="/etc/nginx/sites-available/certidude.conf", | ||||
|     type=click.File(mode="w", atomic=True, lazy=True), | ||||
|     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("--country", "-c", default=None, help="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("--authority-lifetime", default=20*365, help="Authority certificate lifetime in days, 7300 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("--authority-lifetime", default=20*365, help="Authority certificate lifetime in days, 20 years by default") | ||||
| @click.option("--organization", "-o", default=None, help="Company or organization name") | ||||
| @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("--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("--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 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) | ||||
|  | ||||
|     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 | ||||
|     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_crt = os.path.join(directory, "ca_crt.pem") | ||||
|  | ||||
|     if not static_path.endswith("/"): | ||||
|         static_path += "/" | ||||
|  | ||||
|     if os.getuid() == 0: | ||||
|         try: | ||||
|             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__)) | ||||
|         certidude_path = sys.argv[0] | ||||
|  | ||||
|         # Push server config generation | ||||
|         if not os.path.exists("/etc/nginx"): | ||||
|             click.echo("Directory /etc/nginx does not exist, hence not creating nginx configuration") | ||||
|             listen = "0.0.0.0" | ||||
| @@ -924,7 +878,6 @@ def certidude_setup_authority(username, static_path, kerberos_keytab, nginx_conf | ||||
|             ).add_extension( | ||||
|                 x509.AuthorityKeyIdentifier.from_issuer_public_key(key.public_key()), | ||||
|                 critical=False | ||||
|  | ||||
|             ) | ||||
|  | ||||
|         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 pycountry import countries | ||||
|  | ||||
|     def dump_common(j): | ||||
|  | ||||
|         person = [j for j in (j.given_name, j.surname) if j] | ||||
|         if person: | ||||
|             click.echo("Associated person: %s" % " ".join(person) + (" <%s>" % j.email_address if j.email_address else "")) | ||||
|         elif j.email_address: | ||||
|             click.echo("Associated e-mail: " + j.email_address) | ||||
|  | ||||
|         bits = [j for j in ( | ||||
|             countries.get(alpha2=j.country_code.upper()).name if | ||||
|             j.country_code else "", | ||||
|             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) | ||||
|  | ||||
|     def dump_common(common_name, path, cert): | ||||
|         click.echo("certidude revoke %s" % common_name) | ||||
|         with open(path, "rb") as fh: | ||||
|             buf = fh.read() | ||||
|             click.echo("md5sum: %s" % hashlib.md5(buf).hexdigest()) | ||||
|             click.echo("sha1sum: %s" % hashlib.sha1(buf).hexdigest()) | ||||
|             click.echo("sha256sum: %s" % hashlib.sha256(buf).hexdigest()) | ||||
|         click.echo() | ||||
|         for ext in cert.extensions: | ||||
|             print " -", ext.value | ||||
|         click.echo() | ||||
|  | ||||
|     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: | ||||
|                 click.echo("s " + j.path + " " + j.identity) | ||||
|                 click.echo("s " + path) | ||||
|                 continue | ||||
|             click.echo(click.style(j.common_name, fg="blue")) | ||||
|             click.echo("=" * len(j.common_name)) | ||||
|             click.echo("State: ? " + click.style("submitted", fg="yellow") + " " + naturaltime(j.created) + click.style(", %s" %j.created,  fg="white")) | ||||
|             click.echo(click.style(common_name, fg="blue")) | ||||
|             click.echo("=" * len(common_name)) | ||||
|             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: | ||||
|         for j in authority.list_signed(): | ||||
|         for common_name, path, buf, cert, server in authority.list_signed(): | ||||
|             if not verbose: | ||||
|                 if j.signed < NOW and j.expires > NOW: | ||||
|                     click.echo("v " + j.path + " " + j.identity) | ||||
|                 elif NOW > j.expires: | ||||
|                     click.echo("e " + j.path + " " + j.identity) | ||||
|                 if cert.not_valid_before < NOW and cert.not_valid_after > NOW: | ||||
|                     click.echo("v " + path) | ||||
|                 elif NOW > cert.not_valid_after: | ||||
|                     click.echo("e " + path) | ||||
|                 else: | ||||
|                     click.echo("y " + j.path + " " + j.identity) | ||||
|                     click.echo("y " + path) | ||||
|                 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)) | ||||
|  | ||||
|             if j.signed < NOW and j.expires > NOW: | ||||
|                 click.echo("Status: \u2713 " + click.style("valid", fg="green") + " " + naturaltime(j.expires) + click.style(", %s" %j.expires,  fg="white")) | ||||
|             elif NOW > j.expires: | ||||
|                 click.echo("Status: \u2717 " + click.style("expired", fg="red") + " " + naturaltime(j.expires) + click.style(", %s" %j.expires,  fg="white")) | ||||
|             click.echo(click.style(common_name, fg="blue") + " " + click.style("%x" % cert.serial_number, fg="white")) | ||||
|             click.echo("="*(len(common_name)+60)) | ||||
|             expires = 0 # TODO | ||||
|             if cert.not_valid_before < NOW and cert.not_valid_after > NOW: | ||||
|                 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 > cert.not_valid_after: | ||||
|                 click.echo("Status: " + click.style("expired", fg="red") + " " + naturaltime(expires) + click.style(", %s" %expires,  fg="white")) | ||||
|             else: | ||||
|                 click.echo("Status: \u2717 " + click.style("not valid yet", fg="red") + click.style(", %s" %j.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("Status: " + click.style("not valid yet", fg="red") + click.style(", %s" %expires,  fg="white")) | ||||
|             click.echo() | ||||
|             click.echo("openssl x509 -in %s -text -noout" % path) | ||||
|             dump_common(common_name, path, cert) | ||||
|  | ||||
|     if show_revoked: | ||||
|         for j in authority.list_revoked(): | ||||
|         for common_name, path, buf, cert, server in authority.list_revoked(): | ||||
|             if not verbose: | ||||
|                 click.echo("r " + j.path + " " + j.identity) | ||||
|                 click.echo("r " + path) | ||||
|                 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("Status: \u2717 " + click.style("revoked", fg="red") + " %s%s" % (naturaltime(NOW-j.changed), click.style(", %s" % j.changed, fg="white"))) | ||||
|             dump_common(j) | ||||
|             if show_path: | ||||
|                 click.echo("Details: openssl x509 -in %s -text -noout" % j.path) | ||||
|             click.echo() | ||||
|  | ||||
|             click.echo(click.style(common_name, fg="blue") + " " + click.style("%x" % cert.serial, fg="white")) | ||||
|             click.echo("="*(len(common_name)+60)) | ||||
|  | ||||
|             _, _, _, _, _, _, _, _, mtime, _ = os.stat(path) | ||||
|             changed = datetime.fromtimestamp(mtime) | ||||
|             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.command("sign", help="Sign certificates") | ||||
| @click.command("sign", help="Sign certificate") | ||||
| @click.argument("common_name") | ||||
| @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, lifetime): | ||||
|     from certidude import authority, config | ||||
|     request = authority.get_request(common_name) | ||||
|     cert = authority.sign(request) | ||||
| def certidude_sign(common_name, overwrite): | ||||
|     from certidude import authority | ||||
|     cert = authority.sign(common_name, overwrite) | ||||
|  | ||||
|  | ||||
| @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.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") | ||||
| @@ -1276,9 +1208,6 @@ def certidude_setup_openvpn(): pass | ||||
| @click.group("setup", help="Getting started section") | ||||
| def certidude_setup(): pass | ||||
|  | ||||
| @click.group("signer", help="Signer process management") | ||||
| def certidude_signer(): pass | ||||
|  | ||||
| @click.group() | ||||
| 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_openvpn) | ||||
| certidude_setup.add_command(certidude_setup_strongswan) | ||||
| certidude_setup.add_command(certidude_setup_client) | ||||
| certidude_setup.add_command(certidude_setup_nginx) | ||||
| entry_point.add_command(certidude_setup) | ||||
| entry_point.add_command(certidude_serve) | ||||
| entry_point.add_command(certidude_signer) | ||||
| entry_point.add_command(certidude_request) | ||||
| entry_point.add_command(certidude_sign) | ||||
| entry_point.add_command(certidude_revoke) | ||||
| entry_point.add_command(certidude_list) | ||||
| entry_point.add_command(certidude_users) | ||||
| entry_point.add_command(certidude_cron) | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     entry_point() | ||||
|   | ||||
| @@ -38,27 +38,31 @@ AUTHORITY_CERTIFICATE_PATH = cp.get("authority", "certificate path") | ||||
| REQUESTS_DIR = cp.get("authority", "requests dir") | ||||
| SIGNED_DIR = cp.get("authority", "signed dir") | ||||
| REVOKED_DIR = cp.get("authority", "revoked dir") | ||||
| EXPIRED_DIR = cp.get("authority", "expired dir") | ||||
|  | ||||
| OUTBOX = cp.get("authority", "outbox uri") | ||||
| OUTBOX_NAME = cp.get("authority", "outbox sender name") | ||||
| OUTBOX_MAIL = cp.get("authority", "outbox sender address") | ||||
|  | ||||
| BUNDLE_FORMAT = cp.get("authority", "bundle format") | ||||
| OPENVPN_BUNDLE_TEMPLATE = cp.get("authority", "openvpn bundle template") | ||||
| BUNDLE_FORMAT = cp.get("bundle", "format") | ||||
| 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 }[ | ||||
|     cp.get("authority", "user certificate enrollment")] | ||||
|     cp.get("authority", "user enrollment")] | ||||
| USER_MULTIPLE_CERTIFICATES = { | ||||
|     "forbidden": False, "single allowed": False, "multiple allowed": True }[ | ||||
|     cp.get("authority", "user certificate enrollment")] | ||||
|     cp.get("authority", "user enrollment")] | ||||
|  | ||||
| CERTIFICATE_BASIC_CONSTRAINTS = "CA:FALSE" | ||||
| CERTIFICATE_KEY_USAGE_FLAGS = "digitalSignature,keyEncipherment" | ||||
| CERTIFICATE_EXTENDED_KEY_USAGE_FLAGS = "clientAuth" | ||||
| CERTIFICATE_LIFETIME = cp.getint("signature", "certificate lifetime") | ||||
| CERTIFICATE_AUTHORITY_URL = cp.get("signature", "certificate url") | ||||
| REQUEST_SUBMISSION_ALLOWED = cp.getboolean("authority", "request submission allowed") | ||||
| CLIENT_CERTIFICATE_LIFETIME = cp.getint("signature", "client certificate lifetime") | ||||
| SERVER_CERTIFICATE_LIFETIME = cp.getint("signature", "server certificate lifetime") | ||||
| AUTHORITY_CERTIFICATE_URL = cp.get("signature", "authority certificate 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") | ||||
|  | ||||
|   | ||||
| @@ -2,12 +2,9 @@ import falcon | ||||
| import ipaddress | ||||
| import json | ||||
| import logging | ||||
| import re | ||||
| import types | ||||
| from datetime import date, time, datetime | ||||
| from OpenSSL import crypto | ||||
| from certidude.auth import User | ||||
| from certidude.wrappers import Request, Certificate | ||||
| from urlparse import urlparse | ||||
|  | ||||
| logger = logging.getLogger("api") | ||||
| @@ -52,21 +49,7 @@ def event_source(func): | ||||
|     return wrapped | ||||
|  | ||||
| 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): | ||||
|         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): | ||||
|             return str(obj) | ||||
|         if isinstance(obj, set): | ||||
| @@ -77,17 +60,9 @@ class MyEncoder(json.JSONEncoder): | ||||
|             return obj.strftime("%Y-%m-%d") | ||||
|         if isinstance(obj, types.GeneratorType): | ||||
|             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): | ||||
|             return dict(name=obj.name, given_name=obj.given_name, | ||||
|                 surname=obj.surname, mail=obj.mail) | ||||
|         if hasattr(obj, "serialize"): | ||||
|             return obj.serialize() | ||||
|         return json.JSONEncoder.default(self, obj) | ||||
|  | ||||
|  | ||||
| @@ -96,29 +71,13 @@ def serialize(func): | ||||
|     Falcon response serialization | ||||
|     """ | ||||
|     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("Pragma", "no-cache") | ||||
|         resp.set_header("Expires", "0") | ||||
|         r = func(instance, req, resp, **kwargs) | ||||
|         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 | ||||
|         resp.body = json.dumps(func(instance, req, resp, **kwargs), cls=MyEncoder) | ||||
|     return wrapped | ||||
|  | ||||
|   | ||||
| @@ -4,18 +4,20 @@ import os | ||||
| import requests | ||||
| import subprocess | ||||
| import tempfile | ||||
| from base64 import b64encode | ||||
| from datetime import datetime, timedelta | ||||
| from certidude import errors, const | ||||
| from certidude.wrappers import Certificate, Request | ||||
| 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.primitives import hashes, serialization | ||||
| from cryptography.hazmat.primitives.serialization import Encoding | ||||
| from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID, AuthorityInformationAccessOID | ||||
| 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 | ||||
|     """ | ||||
| @@ -26,6 +28,8 @@ def certidude_request_certificate(server, key_path, request_path, certificate_pa | ||||
|     if wait: | ||||
|         request_params.add("wait=forever") | ||||
|  | ||||
|     renew = False # Attempt to renew if certificate has expired | ||||
|  | ||||
|     # Expand ca.example.com | ||||
|     scheme = "http" if insecure else "https" # TODO: Expose in CLI | ||||
|     authority_url = "%s://%s/api/certificate/" % (scheme, server) | ||||
| @@ -42,12 +46,13 @@ def certidude_request_certificate(server, key_path, request_path, certificate_pa | ||||
|         try: | ||||
|             r = requests.get(authority_url, | ||||
|                 headers={"Accept": "application/x-x509-ca-cert,application/x-pem-file"}) | ||||
|             cert = crypto.load_certificate(crypto.FILETYPE_PEM, r.text) | ||||
|         except crypto.Error: | ||||
|             raise ValueError("Failed to parse PEM: %s" % r.text) | ||||
|             x509.load_pem_x509_certificate(r.content, default_backend()) | ||||
|         except: | ||||
|             raise | ||||
|         #    raise ValueError("Failed to parse PEM: %s" % r.text) | ||||
|         authority_partial = tempfile.mktemp(prefix=authority_path + ".part") | ||||
|         with open(authority_partial, "w") as oh: | ||||
|             oh.write(r.text) | ||||
|             oh.write(r.content) | ||||
|         click.echo("Writing authority certificate to: %s" % 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 | ||||
|     if os.path.exists(certificate_path): | ||||
|         cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(certificate_path).read()) | ||||
|         revocation_list = crypto.load_crl(crypto.FILETYPE_PEM, open(revocations_path).read()) | ||||
|         for revocation in revocation_list.get_revoked(): | ||||
|             if int(revocation.get_serial(), 16) == cert.get_serial_number(): | ||||
|                 if revocation.get_reason() == "Certificate Hold": # TODO: 'Remove From CRL' | ||||
|                     # TODO: Disable service for time being | ||||
|                     click.echo("Certificate put on hold, doing nothing for now") | ||||
|         cert = x509.load_pem_x509_certificate(open(certificate_path).read(), default_backend()) | ||||
|  | ||||
|         for revocation in x509.load_pem_x509_crl(open(revocations_path).read(), default_backend()): | ||||
|             extension, = revocation.extensions | ||||
|  | ||||
|             if revocation.serial_number == cert.serial_number: | ||||
|                 if extension.value.reason == x509.ReasonFlags.certificate_hold: | ||||
|                     # Don't do anything for now | ||||
|                     # TODO: disable service | ||||
|                     break | ||||
|  | ||||
|                 # Disable the client if operation has been ceased or | ||||
|                 # the certificate has been superseded by other | ||||
|                 if revocation.get_reason() in ("Cessation Of Operation", "Superseded"): | ||||
|                 # Disable the client if operation has been ceased | ||||
|                 if extension.value.reason == x509.ReasonFlags.cessation_of_operation: | ||||
|                     if os.path.exists("/etc/certidude/client.conf"): | ||||
|                         clients.readfp(open("/etc/certidude/client.conf")) | ||||
|                         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")) | ||||
|                             click.echo("Authority operation ceased, disabling in /etc/certidude/client.conf") | ||||
|                     # TODO: Disable related services | ||||
|                 if revocation.get_reason() in ("CA Compromise", "AA Compromise"): | ||||
|                     if os.path.exists(authority_path): | ||||
|                         os.remove(key_path) | ||||
|                     return | ||||
|  | ||||
|                 click.echo("Certificate has been revoked, wiping keys and certificates!") | ||||
|                 if os.path.exists(key_path): | ||||
| @@ -102,9 +106,16 @@ def certidude_request_certificate(server, key_path, request_path, certificate_pa | ||||
|         else: | ||||
|             click.echo("Certificate does not seem to be revoked. Good!") | ||||
|  | ||||
|  | ||||
|     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) | ||||
|         with open(key_path) as fh: | ||||
|             key = serialization.load_pem_private_key( | ||||
|                 fh.read(), | ||||
|                 password=None, | ||||
|                 backend=default_backend()) | ||||
|     except EnvironmentError: | ||||
|  | ||||
|         # Construct private key | ||||
| @@ -146,8 +157,15 @@ def certidude_request_certificate(server, key_path, request_path, certificate_pa | ||||
|     # Update CRL, renew certificate, maybe something extra? | ||||
|  | ||||
|     if os.path.exists(certificate_path): | ||||
|         click.echo("Found certificate: %s" % certificate_path) | ||||
|         # TODO: Check certificate validity, download CRL? | ||||
|         cert_buf = open(certificate_path).read() | ||||
|         cert = x509.load_pem_x509_certificate(cert_buf, default_backend()) | ||||
|         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 | ||||
| @@ -169,10 +187,25 @@ def certidude_request_certificate(server, key_path, request_path, certificate_pa | ||||
|         auth = None | ||||
|  | ||||
|     click.echo("Submitting to %s, waiting for response..." % request_url) | ||||
|     submission = requests.post(request_url, | ||||
|         auth=auth, | ||||
|         data=open(request_path), | ||||
|         headers={"Content-Type": "application/pkcs10", "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 | ||||
|     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() | ||||
|  | ||||
|     try: | ||||
|         cert = crypto.load_certificate(crypto.FILETYPE_PEM, submission.text) | ||||
|     except crypto.Error: | ||||
|         cert = x509.load_pem_x509_certificate(submission.text.encode("ascii"), default_backend()) | ||||
|     except: # TODO: catch correct exceptions | ||||
|         raise ValueError("Failed to parse PEM: %s" % submission.text) | ||||
|  | ||||
|     os.umask(0o022) | ||||
|   | ||||
| @@ -79,10 +79,10 @@ def send(template, to=None, attachments=(), **context): | ||||
|     msg.attach(part1) | ||||
|     msg.attach(part2) | ||||
|  | ||||
|     for attachment in attachments: | ||||
|         part = MIMEBase(*attachment.content_type.split("/")) | ||||
|         part.add_header('Content-Disposition', 'attachment', filename=attachment.suggested_filename) | ||||
|         part.set_payload(attachment.dump()) | ||||
|     for attachment, content_type, suggested_filename in attachments: | ||||
|         part = MIMEBase(*content_type.split("/")) | ||||
|         part.add_header('Content-Disposition', 'attachment', filename=suggested_filename) | ||||
|         part.set_payload(attachment) | ||||
|         msg.attach(part) | ||||
|  | ||||
|     # Gmail employs some sort of IPS | ||||
|   | ||||
| @@ -29,7 +29,8 @@ class SignHandler(asynchat.async_chat): | ||||
|             """ | ||||
|  | ||||
|             builder = x509.CertificateRevocationListBuilder( | ||||
|                 ).last_update(now | ||||
|                 ).last_update( | ||||
|                     now - timedelta(minutes=5) | ||||
|                 ).next_update( | ||||
|                     now + timedelta(seconds=config.REVOCATION_LIST_LIFETIME) | ||||
|                 ).issuer_name(self.server.certificate.issuer | ||||
| @@ -89,9 +90,12 @@ class SignHandler(asynchat.async_chat): | ||||
|                 ).public_key( | ||||
|                     request.public_key() | ||||
|                 ).not_valid_before( | ||||
|                     now - timedelta(hours=1) | ||||
|                     now | ||||
|                 ).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( | ||||
|                     x509.BasicConstraints( | ||||
|                         ca=False, | ||||
| @@ -122,7 +126,7 @@ class SignHandler(asynchat.async_chat): | ||||
|                         x509.AccessDescription( | ||||
|                             AuthorityInformationAccessOID.CA_ISSUERS, | ||||
|                             x509.UniformResourceIdentifier( | ||||
|                                 config.CERTIFICATE_AUTHORITY_URL) | ||||
|                                 config.AUTHORITY_CERTIFICATE_URL) | ||||
|                         ) | ||||
|                     ]), | ||||
|                     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) { | ||||
|     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(); }); | ||||
|     $("#certificate-" + e.data.replace("@", "--").replace(".", "-")).slideUp("normal", function() { $(this).remove(); }); | ||||
|     $("#request-" + slug).slideUp("normal", function() { $(this).remove(); }); | ||||
|     $("#certificate-" + slug).slideUp("normal", function() { $(this).remove(); }); | ||||
|  | ||||
|     $.ajax({ | ||||
|         method: "GET", | ||||
|         url: "/api/signed/" + e.data + "/", | ||||
|         dataType: "json", | ||||
|         success: function(certificate, status, xhr) { | ||||
|             console.info(certificate); | ||||
|             console.info("Retrieved certificate:", certificate); | ||||
|             $("#signed_certificates").prepend( | ||||
|                 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() { | ||||
|                 var q = $("#search").val(); | ||||
|                 $(".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(); | ||||
|                     } else { | ||||
|                         $(e).hide(); | ||||
|   | ||||
| @@ -2,12 +2,10 @@ | ||||
| <section id="about"> | ||||
| <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>You can click <a href="/api/bundle/">here</a> to generate bundle | ||||
| for current user account.</p> | ||||
| {% endif %} | ||||
| <p>Mails will be sent to: {{ session.user.mail }}</p> | ||||
|  | ||||
| {% if session.authority %} | ||||
|  | ||||
| @@ -28,9 +26,9 @@ as such require complete reset of X509 infrastructure if some of them needs to b | ||||
| {% endif %} | ||||
|  | ||||
|  | ||||
| <p>User certificate enrollment: | ||||
| {% if session.authority.user_certificate_enrollment %} | ||||
|     {% if session.authority.user_mutliple_certificates %} | ||||
| <p>User enrollment: | ||||
| {% if session.authority.user_enrollment_allowed %} | ||||
|     {% if session.authority.user_multiple_certificates %} | ||||
|     multiple | ||||
|     {% else %} | ||||
|     single | ||||
| @@ -42,10 +40,20 @@ forbidden | ||||
| </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> | ||||
|     <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> | ||||
| </ul> | ||||
|  | ||||
| @@ -134,13 +142,13 @@ cat example.csr | ||||
| </pre> | ||||
|  | ||||
|       <p>Paste the contents here and click submit:</p> | ||||
|       <textarea id="request_body" style="width:100%; min-height: 4em;" placeholder="-----BEGIN CERTIFICATE REQUEST----- | ||||
| ... | ||||
| -----END CERTIFICATE REQUEST-----"></textarea> | ||||
|       <textarea id="request_body" style="width:100%; min-height: 4em;" placeholder="-----BEGIN CERTIFICATE REQUEST-----"></textarea> | ||||
|       <button class="icon upload" id="request_submit" style="float:none;">Submit</button> | ||||
|     {% else %} | ||||
|       <p>Submit a certificate signing request with Certidude:</p> | ||||
|       <pre>certidude setup client {{session.common_name}}</pre> | ||||
|       <p>Submit a certificate signing request from Mac OS X, Ubuntu or Fedora:</p> | ||||
|       <pre>easy_install pip | ||||
| pip install certidude | ||||
| certidude bootstrap {{session.authority.common_name}}</pre> | ||||
|     {% endif %} | ||||
|  | ||||
|     <ul id="pending_requests"> | ||||
| @@ -180,7 +188,8 @@ cat example.csr | ||||
|     <h1>Revoked certificates</h1> | ||||
|     <p>To fetch <a href="{{window.location.href}}api/revoked/">certificate revocation list</a>:</p> | ||||
|     <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> | ||||
|  | ||||
|   | ||||
| @@ -1,13 +1,18 @@ | ||||
| <li id="request-{{ request.common_name | replace('@', '--') | replace('.', '-') }}" class="filterable"> | ||||
|  | ||||
| <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 revoke" onClick="javascript:$(this).addClass('busy');$.ajax({url:'/api/request/{{request.common_name}}/',type:'delete'});">Delete</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}}/?sha256sum={{ request.sha256sum }}',type:'delete'});">Delete</button> | ||||
|  | ||||
|  | ||||
| <div class="monospace"> | ||||
| {% if request.server %} | ||||
| {% include 'img/iconmonstr-server-1.svg' %} | ||||
| {% else %} | ||||
| {% include 'img/iconmonstr-certificate-15.svg' %} | ||||
| {{request.identity}} | ||||
| {% endif %} | ||||
|  | ||||
| {{request.common_name}} | ||||
| </div> | ||||
|  | ||||
| {% if request.email_address %} | ||||
| @@ -16,7 +21,7 @@ | ||||
|  | ||||
| <div class="monospace"> | ||||
| {% include 'img/iconmonstr-key-3.svg' %} | ||||
| <span title="SHA-1 of public key"> | ||||
| <span title="SHA-256 of certificate signing request"> | ||||
| {{ request.sha256sum }} | ||||
| </span> | ||||
| {{ 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> | ||||
|     <button class="icon revoke" onClick="javascript:$(this).addClass('busy');$.ajax({url:'/api/signed/{{certificate.common_name}}/',type:'delete'});">Revoke</button> | ||||
|  | ||||
|     <div class="monospace"> | ||||
|     {% if certificate.server %} | ||||
|     {% include 'img/iconmonstr-server-1.svg' %} | ||||
|     {% else %} | ||||
|     {% include 'img/iconmonstr-certificate-15.svg' %} | ||||
|     {% endif %} | ||||
|  | ||||
|     {{certificate.common_name}} | ||||
|     </div> | ||||
|  | ||||
|   | ||||
| @@ -78,11 +78,33 @@ database = sqlite://{{ directory }}/db.sqlite | ||||
| openvpn status uri = http://router.example.com/status.log | ||||
|  | ||||
| [signature] | ||||
| certificate lifetime = {{ certificate_lifetime }} | ||||
| revocation list lifetime = {{ revocation_list_lifetime }} | ||||
| certificate url = {{ certificate_url }} | ||||
| # Server certificate is granted to certificate with | ||||
| # common name that includes period which translates to FQDN of the machine. | ||||
| # 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 }} | ||||
|  | ||||
| # 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] | ||||
| event source token = {{ push_token }} | ||||
| 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 | ||||
|  | ||||
| [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 | ||||
| # request bundles. In case of 'single allowed' the common name of the | ||||
| # certificate is set to username, this should work well with REMOTE_USER | ||||
| # enabled web apps running behind Apache/nginx. | ||||
| # In case of 'multiple allowed' the common name is set to username@device-identifier. | ||||
| ;user certificate enrollment = forbidden | ||||
| ;user certificate enrollment = single allowed | ||||
| user certificate enrollment = multiple allowed | ||||
| ;user enrollment = forbidden | ||||
| ;user enrollment = single 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 }} | ||||
| certificate path = {{ ca_crt }} | ||||
| @@ -112,8 +145,10 @@ outbox uri = {{ outbox }} | ||||
| outbox sender name = Certificate management | ||||
| outbox sender address = certificates@example.com | ||||
|  | ||||
| bundle format = p12 | ||||
| ;bundle format = ovpn | ||||
|  | ||||
| openvpn bundle template = /etc/certidude/template.ovpn | ||||
| [bundle] | ||||
| format = p12 | ||||
| ;format = 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. | ||||
|  | ||||
| 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 %}. | ||||
|  | ||||
| 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 | ||||
| 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. | ||||
|  | ||||
|   | ||||
							
								
								
									
										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; | ||||
| ssl_prefer_server_ciphers on; | ||||
| # Following are already enabled by /etc/nginx/nginx.conf | ||||
| #ssl_protocols  TLSv1 TLSv1.1 TLSv1.2; | ||||
| #ssl_prefer_server_ciphers on; | ||||
| 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_dhparam {{dhparam_path}}; | ||||
|   | ||||
| @@ -7,16 +7,22 @@ nobind | ||||
| # OpenVPN gateway(s), uncomment remote-random to load balance | ||||
| comp-lzo | ||||
| proto udp | ||||
| remote 1.2.3.4 | ||||
| ;remote 1.2.3.5 | ||||
| ;remote-random | ||||
| {% if servers %} | ||||
| remote-random | ||||
| {% for server in servers %} | ||||
| remote {{ server }} 51900 | ||||
| {% endfor %} | ||||
| {% else %} | ||||
| remote 1.2.3.4 1194 | ||||
| {% endif %} | ||||
| 
 | ||||
| # Virtual network interface settings | ||||
| dev tun | ||||
| persist-tun | ||||
| 
 | ||||
| # 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 | ||||
| ;auth SHA384 | ||||
| 
 | ||||
| @@ -36,12 +42,12 @@ persist-key | ||||
| </cert> | ||||
| 
 | ||||
| # Revocation list | ||||
| <crl-verify> | ||||
| {{crl}} | ||||
| </crl-verify> | ||||
| # Tunnelblick doens't handle inlined CRL | ||||
| # hard to update as well | ||||
| ;<crl-verify> | ||||
| ;</crl-verify> | ||||
| 
 | ||||
| # Pre-shared key for extra layer of security | ||||
| ;<ta> | ||||
| ;... | ||||
| ;</ta> | ||||
| 
 | ||||
| @@ -84,8 +84,6 @@ class DirectoryConnection(object): | ||||
| class ActiveDirectoryUserManager(object): | ||||
|     def get(self, username): | ||||
|         # TODO: Sanitize username | ||||
|         if "@" in username: | ||||
|             username, _ = username.split("@", 1) | ||||
|         with DirectoryConnection() as conn: | ||||
|             ft = config.LDAP_USER_FILTER % username | ||||
|             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 | ||||
| humanize==0.5.1 | ||||
| ipaddress==1.0.18 | ||||
| Jinja2==2.8 | ||||
| Jinja2==2.9.5 | ||||
| Markdown==2.6.8 | ||||
| pyldap==2.4.28 | ||||
| requests==2.10.0 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user