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