Several updates #4
* Improved offline install docs * Migrated token mechanism backend to SQL * Preliminary token mechanism frontend integration * Add clock skew tolerance for OCSP * Add 'ldap computer filter' support for Kerberized machine enroll * Include OCSP and CRL URL-s in certificates, controlled by profile.conf * Better certificate extension handling * Place DH parameters file in /etc/ssl/dhparam.pem * Always talk to CA over port 8443 for 'certidude enroll' * Hardened frontend nginx config * Separate log files for frontend nginx * Better provisioning heuristics * Add sample site.sh config for LEDE image builder * Add more device profiles for LEDE image builder * Various bugfixes and improvements
							
								
								
									
										18
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						| @@ -336,26 +336,28 @@ To uninstall: | |||||||
| Offline install | Offline install | ||||||
| --------------- | --------------- | ||||||
|  |  | ||||||
| To set up certificate authority in an isolated environment use a | To prepare packages for offline installation use following snippet on a | ||||||
| vanilla Ubuntu 16.04 or container to collect the artifacts: | vanilla Ubuntu 16.04 or container: | ||||||
|  |  | ||||||
| .. code:: bash | .. code:: bash | ||||||
|  |  | ||||||
|  |     rm -fv /var/cache/apt/archives/*.deb /var/cache/certidude/wheels/*.whl | ||||||
|  |     apt install --download-only python3-pip | ||||||
|  |     pip3 wheel --wheel-dir=/var/cache/certidude/wheels -r requirements.txt | ||||||
|  |     pip3 wheel --wheel-dir=/var/cache/certidude/wheels . | ||||||
|  |     tar -cf certidude-client.tar /var/cache/certidude/wheels | ||||||
|     add-apt-repository -y ppa:nginx/stable |     add-apt-repository -y ppa:nginx/stable | ||||||
|     apt-get update -q |     apt-get update -q | ||||||
|     rm -fv /var/cache/apt/archives/*.deb /var/cache/certidude/wheels/*.whl |  | ||||||
|     apt install --download-only python3-markdown python3-pyxattr python3-jinja2 python3-cffi software-properties-common libnginx-mod-nchan nginx-full |     apt install --download-only python3-markdown python3-pyxattr python3-jinja2 python3-cffi software-properties-common libnginx-mod-nchan nginx-full | ||||||
|     pip3 wheel --wheel-dir=/var/cache/certidude/wheels -r requirements.txt |  | ||||||
|     pip3 wheel --wheel-dir=/var/cache/certidude/wheels falcon humanize ipaddress simplepam user-agents python-ldap gssapi |     pip3 wheel --wheel-dir=/var/cache/certidude/wheels falcon humanize ipaddress simplepam user-agents python-ldap gssapi | ||||||
|     pip3 wheel --wheel-dir=/var/cache/certidude/wheels . |     tar -cf certidude-server.tar /var/lib/certidude/assets/ /var/cache/apt/archives/ /var/cache/certidude/wheels | ||||||
|     tar -cf certidude-assets.tar /var/lib/certidude/assets/ /var/cache/apt/archives/ /var/cache/certidude/wheels |  | ||||||
|  |  | ||||||
| Transfer certidude-artifacts.tar to the target machine and execute: | Transfer certidude-server.tar or certidude-client.tar to the target machine and execute: | ||||||
|  |  | ||||||
| .. code:: bash | .. code:: bash | ||||||
|  |  | ||||||
|     rm -fv /var/cache/apt/archives/*.deb /var/cache/certidude/wheels/*.whl |     rm -fv /var/cache/apt/archives/*.deb /var/cache/certidude/wheels/*.whl | ||||||
|     tar -xvf certidude-artifacts.tar -C / |     tar -xvf certidude-*.tar -C / | ||||||
|     dpkg -i /var/cache/apt/archives/*.deb |     dpkg -i /var/cache/apt/archives/*.deb | ||||||
|     pip3 install  --use-wheel --no-index --find-links /var/cache/certidude/wheels/*.whl |     pip3 install  --use-wheel --no-index --find-links /var/cache/certidude/wheels/*.whl | ||||||
|  |  | ||||||
|   | |||||||
| @@ -17,6 +17,7 @@ class NormalizeMiddleware(object): | |||||||
|  |  | ||||||
| def certidude_app(log_handlers=[]): | def certidude_app(log_handlers=[]): | ||||||
|     from certidude import authority, config |     from certidude import authority, config | ||||||
|  |     from certidude.tokens import TokenManager | ||||||
|     from .signed import SignedCertificateDetailResource |     from .signed import SignedCertificateDetailResource | ||||||
|     from .request import RequestListResource, RequestDetailResource |     from .request import RequestListResource, RequestDetailResource | ||||||
|     from .lease import LeaseResource, LeaseDetailResource |     from .lease import LeaseResource, LeaseDetailResource | ||||||
| @@ -36,10 +37,20 @@ def certidude_app(log_handlers=[]): | |||||||
|     app.add_route("/api/signed/{cn}/", SignedCertificateDetailResource(authority)) |     app.add_route("/api/signed/{cn}/", SignedCertificateDetailResource(authority)) | ||||||
|     app.add_route("/api/request/{cn}/", RequestDetailResource(authority)) |     app.add_route("/api/request/{cn}/", RequestDetailResource(authority)) | ||||||
|     app.add_route("/api/request/", RequestListResource(authority)) |     app.add_route("/api/request/", RequestListResource(authority)) | ||||||
|     app.add_route("/api/", SessionResource(authority)) |  | ||||||
|  |  | ||||||
|  |     token_resource = None | ||||||
|  |     token_manager = None | ||||||
|     if config.USER_ENROLLMENT_ALLOWED: # TODO: add token enable/disable flag for config |     if config.USER_ENROLLMENT_ALLOWED: # TODO: add token enable/disable flag for config | ||||||
|         app.add_route("/api/token/", TokenResource(authority)) |         if config.TOKEN_BACKEND == "sql": | ||||||
|  |             token_manager = TokenManager(config.TOKEN_DATABASE) | ||||||
|  |             token_resource = TokenResource(authority, token_manager) | ||||||
|  |             app.add_route("/api/token/", token_resource) | ||||||
|  |         elif not config.TOKEN_BACKEND: | ||||||
|  |             pass | ||||||
|  |         else: | ||||||
|  |             raise NotImplementedError("Token backend '%s' not supported" % config.TOKEN_BACKEND) | ||||||
|  |  | ||||||
|  |     app.add_route("/api/", SessionResource(authority, token_manager)) | ||||||
|  |  | ||||||
|     # Extended attributes for scripting etc. |     # Extended attributes for scripting etc. | ||||||
|     app.add_route("/api/signed/{cn}/attr/", AttributeResource(authority, namespace="machine")) |     app.add_route("/api/signed/{cn}/attr/", AttributeResource(authority, namespace="machine")) | ||||||
|   | |||||||
| @@ -11,4 +11,5 @@ class LogResource(RelationalMixin): | |||||||
|     @authorize_admin |     @authorize_admin | ||||||
|     def on_get(self, req, resp): |     def on_get(self, req, resp): | ||||||
|         # TODO: Add last id parameter |         # TODO: Add last id parameter | ||||||
|         return self.iterfetch("select * from log order by created desc") |         return self.iterfetch("select * from log order by created desc limit ?", | ||||||
|  |             req.get_param_as_int("limit")) | ||||||
|   | |||||||
| @@ -4,8 +4,8 @@ import os | |||||||
| from asn1crypto.util import timezone | from asn1crypto.util import timezone | ||||||
| from asn1crypto import ocsp | from asn1crypto import ocsp | ||||||
| from base64 import b64decode | from base64 import b64decode | ||||||
| from certidude import config | from certidude import config, const | ||||||
| from datetime import datetime | from datetime import datetime, timedelta | ||||||
| from oscrypto import asymmetric | from oscrypto import asymmetric | ||||||
| from .utils import AuthorityHandler | from .utils import AuthorityHandler | ||||||
| from .utils.firewall import whitelist_subnets | from .utils.firewall import whitelist_subnets | ||||||
| @@ -88,7 +88,8 @@ class OCSPResource(AuthorityHandler): | |||||||
|                     'serial_number': serial, |                     'serial_number': serial, | ||||||
|                 }, |                 }, | ||||||
|                 'cert_status': status, |                 'cert_status': status, | ||||||
|                 'this_update': now, |                 'this_update': now - const.CLOCK_SKEW_TOLERANCE, | ||||||
|  |                 'next_update': now + timedelta(minutes=15) + const.CLOCK_SKEW_TOLERANCE, | ||||||
|                 'single_extensions': [] |                 'single_extensions': [] | ||||||
|             }) |             }) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ from base64 import b64decode | |||||||
| from certidude import config, push, errors | from certidude import config, push, errors | ||||||
| from certidude.decorators import csrf_protection, MyEncoder | from certidude.decorators import csrf_protection, MyEncoder | ||||||
| from certidude.profile import SignatureProfile | from certidude.profile import SignatureProfile | ||||||
|  | from certidude.user import DirectoryConnection | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from oscrypto import asymmetric | from oscrypto import asymmetric | ||||||
| from oscrypto.errors import SignatureError | from oscrypto.errors import SignatureError | ||||||
| @@ -84,13 +85,28 @@ class RequestListResource(AuthorityHandler): | |||||||
|                             "Bad request", |                             "Bad request", | ||||||
|                             "Common name %s differs from Kerberos credential %s!" % (common_name, machine)) |                             "Common name %s differs from Kerberos credential %s!" % (common_name, machine)) | ||||||
|  |  | ||||||
|  |                     hit = False | ||||||
|  |                     with DirectoryConnection() as conn: | ||||||
|  |                         ft = config.LDAP_COMPUTER_FILTER % ("%s$" % machine) | ||||||
|  |                         attribs = "cn", | ||||||
|  |                         r = conn.search_s(config.LDAP_BASE, 2, ft, attribs) | ||||||
|  |                         for dn, entry in r: | ||||||
|  |                             if not dn: | ||||||
|  |                                 continue | ||||||
|  |                             else: | ||||||
|  |                                 hit = True | ||||||
|  |                                 break | ||||||
|  |  | ||||||
|  |                     if hit: | ||||||
|                         # Automatic enroll with Kerberos machine cerdentials |                         # Automatic enroll with Kerberos machine cerdentials | ||||||
|                         resp.set_header("Content-Type", "application/x-pem-file") |                         resp.set_header("Content-Type", "application/x-pem-file") | ||||||
|                         cert, resp.body = self.authority._sign(csr, body, |                         cert, resp.body = self.authority._sign(csr, body, | ||||||
|                             profile=config.PROFILES["rw"], overwrite=overwrite_allowed) |                             profile=config.PROFILES["rw"], overwrite=overwrite_allowed) | ||||||
|                     logger.info("Automatically enrolled Kerberos authenticated machine %s from %s", |                         logger.info("Automatically enrolled Kerberos authenticated machine %s (%s) from %s", | ||||||
|                         machine, req.context.get("remote_addr")) |                             machine, dn, req.context.get("remote_addr")) | ||||||
|                         return |                         return | ||||||
|  |                     else: | ||||||
|  |                         logger.error("Kerberos authenticated machine %s didn't fit the 'ldap computer filter' criteria %s" % (machine, ft)) | ||||||
|  |  | ||||||
|  |  | ||||||
|         """ |         """ | ||||||
|   | |||||||
| @@ -18,9 +18,13 @@ class CertificateAuthorityResource(object): | |||||||
|         resp.stream = open(config.AUTHORITY_CERTIFICATE_PATH, "rb") |         resp.stream = open(config.AUTHORITY_CERTIFICATE_PATH, "rb") | ||||||
|         resp.append_header("Content-Type", "application/x-x509-ca-cert") |         resp.append_header("Content-Type", "application/x-x509-ca-cert") | ||||||
|         resp.append_header("Content-Disposition", "attachment; filename=%s.crt" % |         resp.append_header("Content-Disposition", "attachment; filename=%s.crt" % | ||||||
|             const.HOSTNAME.encode("ascii")) |             const.HOSTNAME) | ||||||
|  |  | ||||||
| class SessionResource(AuthorityHandler): | class SessionResource(AuthorityHandler): | ||||||
|  |     def __init__(self, authority, token_manager): | ||||||
|  |         AuthorityHandler.__init__(self, authority) | ||||||
|  |         self.token_manager = token_manager | ||||||
|  |  | ||||||
|     @csrf_protection |     @csrf_protection | ||||||
|     @serialize |     @serialize | ||||||
|     @login_required |     @login_required | ||||||
| @@ -97,7 +101,7 @@ class SessionResource(AuthorityHandler): | |||||||
|                     signer_username = None |                     signer_username = None | ||||||
|  |  | ||||||
|                 # TODO: dedup |                 # TODO: dedup | ||||||
|                 yield dict( |                 serialized = dict( | ||||||
|                     serial = "%x" % cert.serial_number, |                     serial = "%x" % cert.serial_number, | ||||||
|                     organizational_unit = cert.subject.native.get("organizational_unit_name"), |                     organizational_unit = cert.subject.native.get("organizational_unit_name"), | ||||||
|                     common_name = common_name, |                     common_name = common_name, | ||||||
| @@ -109,12 +113,37 @@ class SessionResource(AuthorityHandler): | |||||||
|                     lease = lease, |                     lease = lease, | ||||||
|                     tags = tags, |                     tags = tags, | ||||||
|                     attributes = attributes or None, |                     attributes = attributes or None, | ||||||
|                     extensions = dict([ |                     responder_url = None | ||||||
|                         (e["extn_id"].native, e["extn_value"].native) |  | ||||||
|                         for e in cert["tbs_certificate"]["extensions"] |  | ||||||
|                         if e["extn_id"].native in ("extended_key_usage",)]) |  | ||||||
|                 ) |                 ) | ||||||
|  |  | ||||||
|  |                 for e in cert["tbs_certificate"]["extensions"].native: | ||||||
|  |                     if e["extn_id"] == "key_usage": | ||||||
|  |                         serialized["key_usage"] = e["extn_value"] | ||||||
|  |                     elif e["extn_id"] == "extended_key_usage": | ||||||
|  |                         serialized["extended_key_usage"] = e["extn_value"] | ||||||
|  |                     elif e["extn_id"] == "basic_constraints": | ||||||
|  |                         serialized["basic_constraints"] = e["extn_value"] | ||||||
|  |                     elif e["extn_id"] == "crl_distribution_points": | ||||||
|  |                         for c in e["extn_value"]: | ||||||
|  |                             serialized["revoked_url"] = c["distribution_point"] | ||||||
|  |                             break | ||||||
|  |                         serialized["extended_key_usage"] = e["extn_value"] | ||||||
|  |                     elif e["extn_id"] == "authority_information_access": | ||||||
|  |                         for a in e["extn_value"]: | ||||||
|  |                             if a["access_method"] == "ocsp": | ||||||
|  |                                 serialized["responder_url"] = a["access_location"] | ||||||
|  |                             else: | ||||||
|  |                                 raise NotImplementedError("Don't know how to handle AIA access method %s" % a["access_method"]) | ||||||
|  |                     elif e["extn_id"] == "authority_key_identifier": | ||||||
|  |                         pass | ||||||
|  |                     elif e["extn_id"] == "key_identifier": | ||||||
|  |                         pass | ||||||
|  |                     elif e["extn_id"] == "subject_alt_name": | ||||||
|  |                         serialized["subject_alt_name"], = e["extn_value"] | ||||||
|  |                     else: | ||||||
|  |                         raise NotImplementedError("Don't know how to handle extension %s" % e["extn_id"]) | ||||||
|  |                 yield serialized | ||||||
|  |  | ||||||
|         logger.info("Logged in authority administrator %s from %s with %s" % ( |         logger.info("Logged in authority administrator %s from %s with %s" % ( | ||||||
|             req.context.get("user"), req.context.get("remote_addr"), req.context.get("user_agent"))) |             req.context.get("user"), req.context.get("remote_addr"), req.context.get("user_agent"))) | ||||||
|         return dict( |         return dict( | ||||||
| @@ -130,10 +159,12 @@ class SessionResource(AuthorityHandler): | |||||||
|                 routers = [j[0] for j in self.authority.list_signed( |                 routers = [j[0] for j in self.authority.list_signed( | ||||||
|                     common_name=config.SERVICE_ROUTERS)] |                     common_name=config.SERVICE_ROUTERS)] | ||||||
|             ), |             ), | ||||||
|             authority = dict( |  | ||||||
|             builder = dict( |             builder = dict( | ||||||
|                     profiles = config.IMAGE_BUILDER_PROFILES |                 profiles = config.IMAGE_BUILDER_PROFILES or None | ||||||
|             ), |             ), | ||||||
|  |             authority = dict( | ||||||
|  |                 hostname = const.FQDN, | ||||||
|  |                 tokens = self.token_manager.list() if self.token_manager else None, | ||||||
|                 tagging = [dict(name=t[0], type=t[1], title=t[2]) for t in config.TAG_TYPES], |                 tagging = [dict(name=t[0], type=t[1], title=t[2]) for t in config.TAG_TYPES], | ||||||
|                 lease = dict( |                 lease = dict( | ||||||
|                     offline = 600, # Seconds from last seen activity to consider lease offline, OpenVPN reneg-sec option |                     offline = 600, # Seconds from last seen activity to consider lease offline, OpenVPN reneg-sec option | ||||||
| @@ -145,32 +176,39 @@ class SessionResource(AuthorityHandler): | |||||||
|                     distinguished_name = cert_to_dn(self.authority.certificate), |                     distinguished_name = cert_to_dn(self.authority.certificate), | ||||||
|                     md5sum = hashlib.md5(self.authority.certificate_buf).hexdigest(), |                     md5sum = hashlib.md5(self.authority.certificate_buf).hexdigest(), | ||||||
|                     blob = self.authority.certificate_buf.decode("ascii"), |                     blob = self.authority.certificate_buf.decode("ascii"), | ||||||
|  |                     organization = self.authority.certificate["tbs_certificate"]["subject"].native.get("organization_name"), | ||||||
|  |                     signed = self.authority.certificate["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None), | ||||||
|  |                     expires = self.authority.certificate["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None) | ||||||
|                 ), |                 ), | ||||||
|                 mailer = dict( |                 mailer = dict( | ||||||
|                     name = config.MAILER_NAME, |                     name = config.MAILER_NAME, | ||||||
|                     address = config.MAILER_ADDRESS |                     address = config.MAILER_ADDRESS | ||||||
|                 ) if config.MAILER_ADDRESS else None, |                 ) if config.MAILER_ADDRESS else None, | ||||||
|                 machine_enrollment_subnets=config.MACHINE_ENROLLMENT_SUBNETS, |  | ||||||
|                 user_enrollment_allowed=config.USER_ENROLLMENT_ALLOWED, |                 user_enrollment_allowed=config.USER_ENROLLMENT_ALLOWED, | ||||||
|                 user_multiple_certificates=config.USER_MULTIPLE_CERTIFICATES, |                 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=serialize_requests(self.authority.list_requests), |                 requests=serialize_requests(self.authority.list_requests), | ||||||
|                 signed=serialize_certificates(self.authority.list_signed), |                 signed=serialize_certificates(self.authority.list_signed), | ||||||
|                 revoked=serialize_revoked(self.authority.list_revoked), |                 revoked=serialize_revoked(self.authority.list_revoked), | ||||||
|                 admin_users = User.objects.filter_admins(), |  | ||||||
|                 user_subnets = config.USER_SUBNETS or None, |  | ||||||
|                 autosign_subnets = config.AUTOSIGN_SUBNETS or None, |  | ||||||
|                 request_subnets = config.REQUEST_SUBNETS or None, |  | ||||||
|                 admin_subnets=config.ADMIN_SUBNETS or None, |  | ||||||
|                 signature = dict( |                 signature = dict( | ||||||
|                     revocation_list_lifetime=config.REVOCATION_LIST_LIFETIME, |                     revocation_list_lifetime=config.REVOCATION_LIST_LIFETIME, | ||||||
|                     profiles = sorted([p.serialize() for p in config.PROFILES.values()], key=lambda p:p.get("slug")), |                     profiles = sorted([p.serialize() for p in config.PROFILES.values()], key=lambda p:p.get("slug")), | ||||||
|  |  | ||||||
|                 ) |                 ) | ||||||
|             ), |             ), | ||||||
|  |             authorization = dict( | ||||||
|  |                 admin_users = User.objects.filter_admins(), | ||||||
|  |  | ||||||
|  |                 user_subnets = config.USER_SUBNETS or None, | ||||||
|  |                 autosign_subnets = config.AUTOSIGN_SUBNETS or None, | ||||||
|  |                 request_subnets = config.REQUEST_SUBNETS or None, | ||||||
|  |                 machine_enrollment_subnets=config.MACHINE_ENROLLMENT_SUBNETS or None, | ||||||
|  |                 admin_subnets=config.ADMIN_SUBNETS or None, | ||||||
|  |  | ||||||
|  |                 ocsp_subnets = config.OCSP_SUBNETS or None, | ||||||
|  |                 crl_subnets = config.CRL_SUBNETS or None, | ||||||
|  |                 scep_subnets = config.SCEP_SUBNETS or None, | ||||||
|  |             ), | ||||||
|             features=dict( |             features=dict( | ||||||
|                 ocsp=bool(config.OCSP_SUBNETS), |  | ||||||
|                 crl=bool(config.CRL_SUBNETS), |  | ||||||
|                 token=bool(config.TOKEN_URL), |                 token=bool(config.TOKEN_URL), | ||||||
|                 tagging=True, |                 tagging=True, | ||||||
|                 leases=True, |                 leases=True, | ||||||
|   | |||||||
| @@ -1,11 +1,16 @@ | |||||||
|  | import click | ||||||
|  | import codecs | ||||||
| import falcon | import falcon | ||||||
| import logging | import logging | ||||||
| import hashlib | import os | ||||||
|  | import string | ||||||
| from asn1crypto import pem | from asn1crypto import pem | ||||||
| from asn1crypto.csr import CertificationRequest | from asn1crypto.csr import CertificationRequest | ||||||
| from datetime import datetime | from datetime import datetime, timedelta | ||||||
| from time import time | from time import time | ||||||
| from certidude import mailer | from certidude import mailer, const | ||||||
|  | from certidude.tokens import TokenManager | ||||||
|  | from certidude.relational import RelationalMixin | ||||||
| from certidude.decorators import serialize | from certidude.decorators import serialize | ||||||
| from certidude.user import User | from certidude.user import User | ||||||
| from certidude import config | from certidude import config | ||||||
| @@ -15,33 +20,25 @@ from .utils.firewall import login_required, authorize_admin | |||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
| class TokenResource(AuthorityHandler): | class TokenResource(AuthorityHandler): | ||||||
|  |     def __init__(self, authority, manager): | ||||||
|  |         AuthorityHandler.__init__(self, authority) | ||||||
|  |         self.manager = manager | ||||||
|  |  | ||||||
|  |     def on_get(self, req, resp): | ||||||
|  |         return | ||||||
|  |  | ||||||
|     def on_put(self, req, resp): |     def on_put(self, req, resp): | ||||||
|         # Consume token |         try: | ||||||
|         now = time() |             username, mail, created, expires, profile = self.manager.consume(req.get_param("token", required=True)) | ||||||
|         timestamp = req.get_param_as_int("t", required=True) |         except RelationalMixin.DoesNotExist: | ||||||
|         username = req.get_param("u", required=True) |             raise falcon.HTTPForbidden("Forbidden", "No such token or token expired") | ||||||
|         user = User.objects.get(username) |  | ||||||
|         csum = hashlib.sha256() |  | ||||||
|         csum.update(config.TOKEN_SECRET) |  | ||||||
|         csum.update(username.encode("ascii")) |  | ||||||
|         csum.update(str(timestamp).encode("ascii")) |  | ||||||
|  |  | ||||||
|         margin = 300 # Tolerate 5 minute clock skew as Kerberos does |  | ||||||
|         if csum.hexdigest() != req.get_param("c", required=True): |  | ||||||
|             raise falcon.HTTPForbidden("Forbidden", "Invalid token supplied, did you copy-paste link correctly?") |  | ||||||
|         if now < timestamp - margin: |  | ||||||
|             raise falcon.HTTPForbidden("Forbidden", "Token not valid yet, are you sure server clock is correct?") |  | ||||||
|         if now > timestamp + margin + config.TOKEN_LIFETIME: |  | ||||||
|             raise falcon.HTTPForbidden("Forbidden", "Token expired") |  | ||||||
|  |  | ||||||
|         # At this point consider token to be legitimate |  | ||||||
|         body = req.stream.read(req.content_length) |         body = req.stream.read(req.content_length) | ||||||
|         header, _, der_bytes = pem.unarmor(body) |         header, _, der_bytes = pem.unarmor(body) | ||||||
|         csr = CertificationRequest.load(der_bytes) |         csr = CertificationRequest.load(der_bytes) | ||||||
|         common_name = csr["certification_request_info"]["subject"].native["common_name"] |         common_name = csr["certification_request_info"]["subject"].native["common_name"] | ||||||
|         assert common_name == username or common_name.startswith(username + "@"), "Invalid common name %s" % common_name |         assert common_name == username or common_name.startswith(username + "@"), "Invalid common name %s" % common_name | ||||||
|         try: |         try: | ||||||
|             _, resp.body = self.authority._sign(csr, body, profile="default", |             _, resp.body = self.authority._sign(csr, body, profile=config.PROFILES.get(profile), | ||||||
|                 overwrite=config.TOKEN_OVERWRITE_PERMITTED) |                 overwrite=config.TOKEN_OVERWRITE_PERMITTED) | ||||||
|             resp.set_header("Content-Type", "application/x-pem-file") |             resp.set_header("Content-Type", "application/x-pem-file") | ||||||
|             logger.info("Autosigned %s as proven by token ownership", common_name) |             logger.info("Autosigned %s as proven by token ownership", common_name) | ||||||
| @@ -56,40 +53,7 @@ class TokenResource(AuthorityHandler): | |||||||
|     @login_required |     @login_required | ||||||
|     @authorize_admin |     @authorize_admin | ||||||
|     def on_post(self, req, resp): |     def on_post(self, req, resp): | ||||||
|         # Generate token |         self.manager.issue( | ||||||
|         issuer = req.context.get("user") |             issuer = req.context.get("user"), | ||||||
|         username = req.get_param("username") |             subject = User.objects.get(req.get_param("username", required=True)), | ||||||
|         secondary = req.get_param("mail") |             subject_mail = req.get_param("mail")) | ||||||
|  |  | ||||||
|         if username: |  | ||||||
|             # Otherwise try to look up user so we can derive their e-mail address |  | ||||||
|             user = User.objects.get(username) |  | ||||||
|         else: |  | ||||||
|             # If no username is specified, assume it's intended for someone outside domain |  | ||||||
|             username = "guest-%s" % hashlib.sha256(secondary.encode("ascii")).hexdigest()[-8:] |  | ||||||
|             if not secondary: |  | ||||||
|                 raise |  | ||||||
|  |  | ||||||
|         timestamp = int(time()) |  | ||||||
|         csum = hashlib.sha256() |  | ||||||
|         csum.update(config.TOKEN_SECRET) |  | ||||||
|         csum.update(username.encode("ascii")) |  | ||||||
|         csum.update(str(timestamp).encode("ascii")) |  | ||||||
|         args = "u=%s&t=%d&c=%s&i=%s" % (username, timestamp, csum.hexdigest(), issuer.name) |  | ||||||
|  |  | ||||||
|         # Token lifetime in local time, to select timezone: dpkg-reconfigure tzdata |  | ||||||
|         token_created = datetime.fromtimestamp(timestamp) |  | ||||||
|         token_expires = datetime.fromtimestamp(timestamp + config.TOKEN_LIFETIME) |  | ||||||
|         try: |  | ||||||
|             with open("/etc/timezone") as fh: |  | ||||||
|                 token_timezone = fh.read().strip() |  | ||||||
|         except EnvironmentError: |  | ||||||
|             token_timezone = None |  | ||||||
|         url = "%s#%s" % (config.TOKEN_URL, args) |  | ||||||
|         context = globals() |  | ||||||
|         context.update(locals()) |  | ||||||
|         mailer.send("token.md", to=user, **context) |  | ||||||
|         return { |  | ||||||
|             "token": args, |  | ||||||
|             "url": url, |  | ||||||
|         } |  | ||||||
|   | |||||||
| @@ -110,7 +110,7 @@ def authenticate(optional=False): | |||||||
|             if kerberized: |             if kerberized: | ||||||
|                 if not req.auth.startswith("Negotiate "): |                 if not req.auth.startswith("Negotiate "): | ||||||
|                     raise falcon.HTTPBadRequest("Bad request", |                     raise falcon.HTTPBadRequest("Bad request", | ||||||
|                         "Bad header, expected Negotiate: %s" % req.auth) |                         "Bad header, expected Negotiate") | ||||||
|  |  | ||||||
|                 os.environ["KRB5_KTNAME"] = config.KERBEROS_KEYTAB |                 os.environ["KRB5_KTNAME"] = config.KERBEROS_KEYTAB | ||||||
|  |  | ||||||
| @@ -158,7 +158,7 @@ def authenticate(optional=False): | |||||||
|  |  | ||||||
|             else: |             else: | ||||||
|                 if not req.auth.startswith("Basic "): |                 if not req.auth.startswith("Basic "): | ||||||
|                     raise falcon.HTTPBadRequest("Bad request", "Bad header, expected Basic: %s" % req.auth) |                     raise falcon.HTTPBadRequest("Bad request", "Bad header, expected Basic") | ||||||
|                 basic, token = req.auth.split(" ", 1) |                 basic, token = req.auth.split(" ", 1) | ||||||
|                 user, passwd = b64decode(token).decode("ascii").split(":", 1) |                 user, passwd = b64decode(token).decode("ascii").split(":", 1) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -13,26 +13,14 @@ from asn1crypto.csr import CertificationRequest | |||||||
| from certbuilder import CertificateBuilder | from certbuilder import CertificateBuilder | ||||||
| from certidude import config, push, mailer, const | from certidude import config, push, mailer, const | ||||||
| from certidude import errors | from certidude import errors | ||||||
| from certidude.common import cn_to_dn | from certidude.common import cn_to_dn, generate_serial, random | ||||||
| from crlbuilder import CertificateListBuilder, pem_armor_crl | from crlbuilder import CertificateListBuilder, pem_armor_crl | ||||||
| from csrbuilder import CSRBuilder, pem_armor_csr | from csrbuilder import CSRBuilder, pem_armor_csr | ||||||
| from datetime import datetime, timedelta | from datetime import datetime, timedelta | ||||||
| from jinja2 import Template | from jinja2 import Template | ||||||
| from random import SystemRandom |  | ||||||
| from xattr import getxattr, listxattr, setxattr | from xattr import getxattr, listxattr, setxattr | ||||||
|  |  | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
| random = SystemRandom() |  | ||||||
|  |  | ||||||
| try: |  | ||||||
|     from time import time_ns |  | ||||||
| except ImportError: |  | ||||||
|     from time import time |  | ||||||
|     def time_ns(): |  | ||||||
|         return int(time() * 10**9) # 64 bits integer, 32 ns bits |  | ||||||
|  |  | ||||||
| def generate_serial(): |  | ||||||
|     return time_ns() << 56 | random.randint(0, 2**56-1) |  | ||||||
|  |  | ||||||
| # https://securityblog.redhat.com/2014/06/18/openssl-privilege-separation-analysis/ | # https://securityblog.redhat.com/2014/06/18/openssl-privilege-separation-analysis/ | ||||||
| # https://jamielinux.com/docs/openssl-certificate-authority/ | # https://jamielinux.com/docs/openssl-certificate-authority/ | ||||||
| @@ -61,13 +49,14 @@ def self_enroll(skip_notify=False): | |||||||
|         self_public_key = asymmetric.load_public_key(path) |         self_public_key = asymmetric.load_public_key(path) | ||||||
|         private_key = asymmetric.load_private_key(config.SELF_KEY_PATH) |         private_key = asymmetric.load_private_key(config.SELF_KEY_PATH) | ||||||
|     except FileNotFoundError: # certificate or private key not found |     except FileNotFoundError: # certificate or private key not found | ||||||
|  |         click.echo("Generating private key for frontend: %s" % config.SELF_KEY_PATH) | ||||||
|         with open(config.SELF_KEY_PATH, 'wb') as fh: |         with open(config.SELF_KEY_PATH, 'wb') as fh: | ||||||
|             if public_key.algorithm == "ec": |             if public_key.algorithm == "ec": | ||||||
|                 self_public_key, private_key = asymmetric.generate_pair("ec", curve=public_key.curve) |                 self_public_key, private_key = asymmetric.generate_pair("ec", curve=public_key.curve) | ||||||
|             elif public_key.algorithm == "rsa": |             elif public_key.algorithm == "rsa": | ||||||
|                 self_public_key, private_key = asymmetric.generate_pair("rsa", bit_size=public_key.bit_size) |                 self_public_key, private_key = asymmetric.generate_pair("rsa", bit_size=public_key.bit_size) | ||||||
|             else: |             else: | ||||||
|                 NotImplemented |                 raise NotImplemented("CA certificate public key algorithm %s not supported" % public_key.algorithm) | ||||||
|             fh.write(asymmetric.dump_private_key(private_key, None)) |             fh.write(asymmetric.dump_private_key(private_key, None)) | ||||||
|     else: |     else: | ||||||
|         now = datetime.utcnow() |         now = datetime.utcnow() | ||||||
| @@ -84,10 +73,11 @@ def self_enroll(skip_notify=False): | |||||||
|         drop_privileges() |         drop_privileges() | ||||||
|         assert os.getuid() != 0 and os.getgid() != 0 |         assert os.getuid() != 0 and os.getgid() != 0 | ||||||
|         path = os.path.join(config.REQUESTS_DIR, common_name + ".pem") |         path = os.path.join(config.REQUESTS_DIR, common_name + ".pem") | ||||||
|         click.echo("Writing request to %s" % path) |         click.echo("Writing certificate signing request for frontend: %s" % path) | ||||||
|         with open(path, "wb") as fh: |         with open(path, "wb") as fh: | ||||||
|             fh.write(pem_armor_csr(request)) # Write CSR with certidude permissions |             fh.write(pem_armor_csr(request)) # Write CSR with certidude permissions | ||||||
|         authority.sign(common_name, skip_notify=skip_notify, skip_push=True, overwrite=True, profile=config.PROFILES["srv"]) |         authority.sign(common_name, skip_notify=skip_notify, skip_push=True, overwrite=True, profile=config.PROFILES["srv"]) | ||||||
|  |         click.echo("Frontend certificate signed") | ||||||
|         sys.exit(0) |         sys.exit(0) | ||||||
|     else: |     else: | ||||||
|         os.waitpid(pid, 0) |         os.waitpid(pid, 0) | ||||||
| @@ -409,13 +399,15 @@ def _sign(csr, buf, profile, skip_notify=False, skip_push=False, overwrite=False | |||||||
|     builder.serial_number = generate_serial() |     builder.serial_number = generate_serial() | ||||||
|  |  | ||||||
|     now = datetime.utcnow() |     now = datetime.utcnow() | ||||||
|     builder.begin_date = now - timedelta(minutes=5) |     builder.begin_date = now - const.CLOCK_SKEW_TOLERANCE | ||||||
|     builder.end_date = now + timedelta(days=profile.lifetime) |     builder.end_date = now + timedelta(days=profile.lifetime) | ||||||
|     builder.issuer = certificate |     builder.issuer = certificate | ||||||
|     builder.ca = profile.ca |     builder.ca = profile.ca | ||||||
|     builder.key_usage = profile.key_usage |     builder.key_usage = profile.key_usage | ||||||
|     builder.extended_key_usage = profile.extended_key_usage |     builder.extended_key_usage = profile.extended_key_usage | ||||||
|     builder.subject_alt_domains = [common_name] |     builder.subject_alt_domains = [common_name] | ||||||
|  |     builder.ocsp_url = profile.responder_url | ||||||
|  |     builder.crl_url = profile.revoked_url | ||||||
|  |  | ||||||
|     end_entity_cert = builder.build(private_key) |     end_entity_cert = builder.build(private_key) | ||||||
|     end_entity_cert_buf = asymmetric.dump_certificate(end_entity_cert) |     end_entity_cert_buf = asymmetric.dump_certificate(end_entity_cert) | ||||||
|   | |||||||
							
								
								
									
										148
									
								
								certidude/cli.py
									
									
									
									
									
								
							
							
						
						| @@ -18,7 +18,7 @@ from certbuilder import CertificateBuilder, pem_armor_certificate | |||||||
| from certidude import const | from certidude import const | ||||||
| from csrbuilder import CSRBuilder, pem_armor_csr | from csrbuilder import CSRBuilder, pem_armor_csr | ||||||
| from configparser import ConfigParser, NoOptionError | from configparser import ConfigParser, NoOptionError | ||||||
| from certidude.common import apt, rpm, drop_privileges, selinux_fixup, cn_to_dn | from certidude.common import apt, rpm, drop_privileges, selinux_fixup, cn_to_dn, generate_serial | ||||||
| from datetime import datetime, timedelta | from datetime import datetime, timedelta | ||||||
| from glob import glob | from glob import glob | ||||||
| from ipaddress import ip_network | from ipaddress import ip_network | ||||||
| @@ -51,7 +51,7 @@ def setup_client(prefix="client_", dh=False): | |||||||
|             authority = arguments.get("authority") |             authority = arguments.get("authority") | ||||||
|             b = os.path.join("/etc/certidude/authority", authority) |             b = os.path.join("/etc/certidude/authority", authority) | ||||||
|             if dh: |             if dh: | ||||||
|                 path = os.path.join(const.STORAGE_PATH, "dh.pem") |                 path = os.path.join("/etc/ssl/dhparam.pem") | ||||||
|                 if not os.path.exists(path): |                 if not os.path.exists(path): | ||||||
|                     rpm("openssl") |                     rpm("openssl") | ||||||
|                     apt("openssl") |                     apt("openssl") | ||||||
| @@ -390,7 +390,6 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self): | |||||||
|             } |             } | ||||||
|  |  | ||||||
|             if renew: # Do mutually authenticated TLS handshake |             if renew: # Do mutually authenticated TLS handshake | ||||||
|                 request_url = "https://%s:8443/api/request/" % authority_name |  | ||||||
|                 kwargs["cert"] = certificate_path, key_path |                 kwargs["cert"] = certificate_path, key_path | ||||||
|                 click.echo("Renewing using current keypair at %s %s" % kwargs["cert"]) |                 click.echo("Renewing using current keypair at %s %s" % kwargs["cert"]) | ||||||
|             else: |             else: | ||||||
| @@ -417,8 +416,8 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self): | |||||||
|                         kwargs["auth"] = HTTPKerberosAuth(mutual_authentication=OPTIONAL, force_preemptive=True) |                         kwargs["auth"] = HTTPKerberosAuth(mutual_authentication=OPTIONAL, force_preemptive=True) | ||||||
|                 else: |                 else: | ||||||
|                     click.echo("Not using machine keytab") |                     click.echo("Not using machine keytab") | ||||||
|                 request_url = "https://%s/api/request/" % authority_name |  | ||||||
|  |  | ||||||
|  |             request_url = "https://%s:8443/api/request/" % authority_name | ||||||
|             if request_params: |             if request_params: | ||||||
|                 request_url = request_url + "?" + "&".join(request_params) |                 request_url = request_url + "?" + "&".join(request_params) | ||||||
|             submission = requests.post(request_url, **kwargs) |             submission = requests.post(request_url, **kwargs) | ||||||
| @@ -580,7 +579,7 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self): | |||||||
|                 nm_config.set("vpn", "key", key_path) |                 nm_config.set("vpn", "key", key_path) | ||||||
|                 nm_config.set("vpn", "cert", certificate_path) |                 nm_config.set("vpn", "cert", certificate_path) | ||||||
|                 nm_config.set("vpn", "ca", authority_path) |                 nm_config.set("vpn", "ca", authority_path) | ||||||
|                 nm_config.set("vpn", "tls-cipher", "TLS-%s-WITH-AES-128-GCM-SHA384" % ( |                 nm_config.set("vpn", "tls-cipher", "TLS-%s-WITH-AES-256-GCM-SHA384" % ( | ||||||
|                     "ECDHE-ECDSA" if authority_public_key.algorithm == "ec" else "DHE-RSA")) |                     "ECDHE-ECDSA" if authority_public_key.algorithm == "ec" else "DHE-RSA")) | ||||||
|                 nm_config.set("vpn", "cipher", "AES-128-GCM") |                 nm_config.set("vpn", "cipher", "AES-128-GCM") | ||||||
|                 nm_config.set("vpn", "auth", "SHA384") |                 nm_config.set("vpn", "auth", "SHA384") | ||||||
| @@ -995,6 +994,10 @@ def certidude_setup_openvpn_networkmanager(authority, remote, common_name, **pat | |||||||
|     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("--tls-config", | ||||||
|  |     default="/etc/nginx/conf.d/tls.conf", | ||||||
|  |     type=click.File(mode="w", atomic=True, lazy=True), | ||||||
|  |     help="TLS configuration file of nginx, /etc/nginx/conf.d/tls.conf by default") | ||||||
| @click.option("--common-name", "-cn", default=const.FQDN, help="Common name of the server, %s by default" % const.FQDN) | @click.option("--common-name", "-cn", default=const.FQDN, help="Common name of the server, %s by default" % const.FQDN) | ||||||
| @click.option("--title", "-t", default="Certidude at %s" % const.FQDN, help="Common name of the certificate authority, 'Certidude at %s' by default" % const.FQDN) | @click.option("--title", "-t", default="Certidude at %s" % const.FQDN, help="Common name of the certificate authority, 'Certidude at %s' by default" % const.FQDN) | ||||||
| @click.option("--authority-lifetime", default=20*365, help="Authority certificate lifetime in days, 20 years by default") | @click.option("--authority-lifetime", default=20*365, help="Authority certificate lifetime in days, 20 years by default") | ||||||
| @@ -1008,7 +1011,7 @@ def certidude_setup_openvpn_networkmanager(authority, remote, common_name, **pat | |||||||
| @click.option("--elliptic-curve", "-e", is_flag=True, help="Generate EC instead of RSA keypair") | @click.option("--elliptic-curve", "-e", is_flag=True, help="Generate EC instead of RSA keypair") | ||||||
| @click.option("--subordinate", is_flag=True, help="Set up subordinate CA instead of root CA") | @click.option("--subordinate", is_flag=True, help="Set up subordinate CA instead of root CA") | ||||||
| @fqdn_required | @fqdn_required | ||||||
| def certidude_setup_authority(username, kerberos_keytab, nginx_config, organization, organizational_unit, common_name, directory, authority_lifetime, push_server, outbox, title, skip_assets, skip_packages, elliptic_curve, subordinate): | def certidude_setup_authority(username, kerberos_keytab, nginx_config, tls_config, organization, organizational_unit, common_name, directory, authority_lifetime, push_server, outbox, title, skip_assets, skip_packages, elliptic_curve, subordinate): | ||||||
|     assert subprocess.check_output(["/usr/bin/lsb_release", "-cs"]) in (b"trusty\n", b"xenial\n", b"bionic\n"), "Only Ubuntu 16.04 supported at the moment" |     assert subprocess.check_output(["/usr/bin/lsb_release", "-cs"]) in (b"trusty\n", b"xenial\n", b"bionic\n"), "Only Ubuntu 16.04 supported at the moment" | ||||||
|     assert os.getuid() == 0 and os.getgid() == 0, "Authority can be set up only by root" |     assert os.getuid() == 0 and os.getgid() == 0, "Authority can be set up only by root" | ||||||
|  |  | ||||||
| @@ -1027,21 +1030,28 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat | |||||||
|             libkrb5-dev libldap2-dev libsasl2-dev gawk libncurses5-dev \ |             libkrb5-dev libldap2-dev libsasl2-dev gawk libncurses5-dev \ | ||||||
|             rsync attr wget unzip" |             rsync attr wget unzip" | ||||||
|         click.echo("Running: %s" % cmd) |         click.echo("Running: %s" % cmd) | ||||||
|         if os.system(cmd): sys.exit(254) |         if os.system(cmd): | ||||||
|         if os.system("pip3 install -q --upgrade gssapi falcon humanize ipaddress simplepam user-agents"): sys.exit(253) |             raise click.ClickException("Failed to install APT packages") | ||||||
|         if os.system("pip3 install -q --pre --upgrade python-ldap"): exit(252) |         if os.system("pip3 install -q --upgrade gssapi falcon humanize ipaddress simplepam user-agents"): | ||||||
|  |             raise click.ClickException("Failed to install Python packages") | ||||||
|  |         if os.system("pip3 install -q --pre --upgrade python-ldap"): | ||||||
|  |             raise click.ClickException("Failed to install python-ldap") | ||||||
|  |  | ||||||
|         if not os.path.exists("/usr/lib/nginx/modules/ngx_nchan_module.so"): |         if not os.path.exists("/usr/lib/nginx/modules/ngx_nchan_module.so"): | ||||||
|             click.echo("Enabling nginx PPA") |             click.echo("Enabling nginx PPA") | ||||||
|             if os.system("add-apt-repository -y ppa:nginx/stable"): sys.exit(251) |             if os.system("add-apt-repository -y ppa:nginx/stable"): | ||||||
|             if os.system("apt-get update -q"): sys.exit(250) |                 raise click.ClickException("Failed to add nginx PPA") | ||||||
|             if os.system("apt-get install -y -q libnginx-mod-nchan"): sys.exit(249) |             if os.system("apt-get update -q"): | ||||||
|  |                 raise click.ClickException("Failed to update package lists") | ||||||
|  |             if os.system("apt-get install -y -q libnginx-mod-nchan"): | ||||||
|  |                 raise click.ClickException("Failed to install nchan") | ||||||
|         else: |         else: | ||||||
|             click.echo("PPA for nginx already enabled") |             click.echo("PPA for nginx already enabled") | ||||||
|  |  | ||||||
|         if not os.path.exists("/usr/sbin/nginx"): |         if not os.path.exists("/usr/sbin/nginx"): | ||||||
|             click.echo("Installing nginx from PPA") |             click.echo("Installing nginx from PPA") | ||||||
|             if os.system("apt-get install -y -q nginx"): sys.exit(248) |             if os.system("apt-get install -y -q nginx"): | ||||||
|  |                 raise click.ClickException("Failed to install nginx") | ||||||
|         else: |         else: | ||||||
|             click.echo("Web server nginx already installed") |             click.echo("Web server nginx already installed") | ||||||
|  |  | ||||||
| @@ -1049,7 +1059,7 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat | |||||||
|             os.symlink("/usr/bin/nodejs", "/usr/bin/node") |             os.symlink("/usr/bin/nodejs", "/usr/bin/node") | ||||||
|  |  | ||||||
|     # Generate secret for tokens |     # Generate secret for tokens | ||||||
|     token_secret = ''.join(random.choice(string.ascii_letters + string.digits + '!@#$%^&*()') for i in range(50)) |     token_url = "https://" + const.FQDN + "/#action=enroll&token=%(token)s&router=%(router)s&protocol=ovpn" | ||||||
|  |  | ||||||
|     template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates", "profile") |     template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates", "profile") | ||||||
|     click.echo("Using templates from %s" % template_path) |     click.echo("Using templates from %s" % template_path) | ||||||
| @@ -1062,6 +1072,10 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat | |||||||
|     revoked_url = "http://%s/api/revoked/" % common_name |     revoked_url = "http://%s/api/revoked/" % common_name | ||||||
|     click.echo("Setting revocation list URL to %s" % revoked_url) |     click.echo("Setting revocation list URL to %s" % revoked_url) | ||||||
|  |  | ||||||
|  |     responder_url = "http://%s/api/ocsp/" % common_name | ||||||
|  |     click.echo("Setting OCSP responder URL to %s" % responder_url) | ||||||
|  |  | ||||||
|  |  | ||||||
|     # Expand variables |     # Expand variables | ||||||
|     assets_dir = os.path.join(directory, "assets") |     assets_dir = os.path.join(directory, "assets") | ||||||
|     ca_key = os.path.join(directory, "ca_key.pem") |     ca_key = os.path.join(directory, "ca_key.pem") | ||||||
| @@ -1070,6 +1084,7 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat | |||||||
|     self_key = os.path.join(directory, "self_key.pem") |     self_key = os.path.join(directory, "self_key.pem") | ||||||
|     sqlite_path = os.path.join(directory, "meta", "db.sqlite") |     sqlite_path = os.path.join(directory, "meta", "db.sqlite") | ||||||
|     distinguished_name = cn_to_dn("Certidude at %s" % common_name, common_name, o=organization, ou=organizational_unit) |     distinguished_name = cn_to_dn("Certidude at %s" % common_name, common_name, o=organization, ou=organizational_unit) | ||||||
|  |     dhparam_path = "/etc/ssl/dhparam.pem" | ||||||
|  |  | ||||||
|     # Builder variables |     # Builder variables | ||||||
|     dhgroup = "ecp384" if elliptic_curve else "modp2048" |     dhgroup = "ecp384" if elliptic_curve else "modp2048" | ||||||
| @@ -1080,8 +1095,7 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat | |||||||
|     except KeyError: |     except KeyError: | ||||||
|         cmd = "adduser", "--system", "--no-create-home", "--group", "certidude" |         cmd = "adduser", "--system", "--no-create-home", "--group", "certidude" | ||||||
|         if subprocess.call(cmd): |         if subprocess.call(cmd): | ||||||
|             click.echo("Failed to create system user 'certidude'") |             raise click.ClickException("Failed to create system user 'certidude'") | ||||||
|             return 255 |  | ||||||
|  |  | ||||||
|     if os.path.exists(kerberos_keytab): |     if os.path.exists(kerberos_keytab): | ||||||
|         click.echo("Service principal keytab found in '%s'" % kerberos_keytab) |         click.echo("Service principal keytab found in '%s'" % kerberos_keytab) | ||||||
| @@ -1114,6 +1128,7 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat | |||||||
|     letsencrypt_fullchain = "/etc/letsencrypt/live/%s/fullchain.pem" % common_name |     letsencrypt_fullchain = "/etc/letsencrypt/live/%s/fullchain.pem" % common_name | ||||||
|     letsencrypt_privkey = "/etc/letsencrypt/live/%s/privkey.pem" % common_name |     letsencrypt_privkey = "/etc/letsencrypt/live/%s/privkey.pem" % common_name | ||||||
|     letsencrypt = os.path.exists(letsencrypt_fullchain) |     letsencrypt = os.path.exists(letsencrypt_fullchain) | ||||||
|  |  | ||||||
|     doc_path = os.path.join(os.path.realpath(os.path.dirname(os.path.dirname(__file__))), "doc") |     doc_path = os.path.join(os.path.realpath(os.path.dirname(os.path.dirname(__file__))), "doc") | ||||||
|     script_dir = os.path.join(os.path.realpath(os.path.dirname(__file__)), "templates", "script") |     script_dir = os.path.join(os.path.realpath(os.path.dirname(__file__)), "templates", "script") | ||||||
|  |  | ||||||
| @@ -1163,26 +1178,27 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat | |||||||
|         if skip_packages: |         if skip_packages: | ||||||
|             click.echo("Not attempting to install packages from NPM as requested...") |             click.echo("Not attempting to install packages from NPM as requested...") | ||||||
|         else: |         else: | ||||||
|             cmd = "npm install --silent -g nunjucks@2.5.2 nunjucks-date@1.2.0 node-forge bootstrap@4.0.0-alpha.6 jquery timeago tether font-awesome qrcode-svg" |             cmd = "npm install --silent --no-optional -g nunjucks@2.5.2 nunjucks-date@1.2.0 node-forge bootstrap@4.0.0-alpha.6 jquery timeago tether font-awesome qrcode-svg" | ||||||
|             click.echo("Installing JavaScript packages: %s" % cmd) |             click.echo("Installing JavaScript packages: %s" % cmd) | ||||||
|             if os.system(cmd): sys.exit(230) |  | ||||||
|  |  | ||||||
|         if skip_assets: |         if skip_assets: | ||||||
|             click.echo("Not attempting to assemble assets as requested...") |             click.echo("Not attempting to assemble assets as requested...") | ||||||
|         else: |         else: | ||||||
|             # Copy fonts |             # Copy fonts | ||||||
|             click.echo("Copying fonts...") |             click.echo("Copying fonts...") | ||||||
|             if os.system("rsync -avq /usr/local/lib/node_modules/font-awesome/fonts/ %s/fonts/" % assets_dir): sys.exit(229) |             if os.system("rsync -avq /usr/local/lib/node_modules/font-awesome/fonts/ %s/fonts/" % assets_dir): | ||||||
|  |                 raise click.ClickException("Failed to copy fonts") | ||||||
|  |  | ||||||
|             # Compile nunjucks templates |             # Compile nunjucks templates | ||||||
|             cmd = 'nunjucks-precompile --include ".html$" --include ".ps1$" --include ".sh$" --include ".svg" %s > %s.part' % (static_path, bundle_js) |             cmd = 'nunjucks-precompile --include "\.html$" --include "\.ps1$" --include "\.sh$" --include "\.svg$" --include "\.yml$" --include "\.conf$" --include "\.mobileconfig$" %s > %s.part' % (static_path, bundle_js) | ||||||
|             click.echo("Compiling templates: %s" % cmd) |             click.echo("Compiling templates: %s" % cmd) | ||||||
|             if os.system(cmd): sys.exit(228) |             if os.system(cmd): | ||||||
|  |                 raise click.ClickException("Failed to compile nunjucks templates") | ||||||
|  |  | ||||||
|             # Assemble bundle.js |             # Assemble bundle.js | ||||||
|             click.echo("Assembling %s" % bundle_js) |             click.echo("Assembling %s" % bundle_js) | ||||||
|             with open(bundle_js + ".part", "a") as fh: |             with open(bundle_js + ".part", "a") as fh: | ||||||
|                 for pkg in "qrcode-svg/dist/qrcode.min.js", "jquery/dist/jquery.min.js", "timeago/*.js", "nunjucks/browser/nunjucks-slim.min.js", "tether/dist/js/*.min.js", "bootstrap/dist/js/*.min.js": |                 for pkg in "jquery/dist/jquery.min.js", "tether/dist/js/*.min.js", "bootstrap/dist/js/*.min.js", "node-forge/dist/forge.all.min.js", "qrcode-svg/dist/qrcode.min.js", "timeago/*.js", "nunjucks/browser/nunjucks-slim.min.js": | ||||||
|                     for j in glob(os.path.join("/usr/local/lib/node_modules", pkg)): |                     for j in glob(os.path.join("/usr/local/lib/node_modules", pkg)): | ||||||
|                         click.echo("- Merging: %s" % j) |                         click.echo("- Merging: %s" % j) | ||||||
|                         with open(j) as ih: |                         with open(j) as ih: | ||||||
| @@ -1208,9 +1224,22 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat | |||||||
|         if not os.path.exists(const.CONFIG_DIR): |         if not os.path.exists(const.CONFIG_DIR): | ||||||
|             click.echo("Creating %s" % const.CONFIG_DIR) |             click.echo("Creating %s" % const.CONFIG_DIR) | ||||||
|             os.makedirs(const.CONFIG_DIR) |             os.makedirs(const.CONFIG_DIR) | ||||||
|  |         if not os.path.exists(const.SCRIPT_DIR): | ||||||
|  |             click.echo("Creating %s" % const.SCRIPT_DIR) | ||||||
|  |             os.makedirs(const.SCRIPT_DIR) | ||||||
|  |  | ||||||
|         os.umask(0o177) # 600 |         os.umask(0o177) # 600 | ||||||
|  |  | ||||||
|  |         if not os.path.exists(dhparam_path): | ||||||
|  |             cmd = "openssl", "dhparam", "-out", dhparam_path, ("1024" if os.getenv("TRAVIS") else str(const.KEY_SIZE)) | ||||||
|  |             subprocess.check_call(cmd) | ||||||
|  |  | ||||||
|  |         if os.path.exists(tls_config.name): | ||||||
|  |             click.echo("Configuration file %s already exists, not overwriting" % tls_config.name) | ||||||
|  |         else: | ||||||
|  |             tls_config.write(env.get_template("nginx-tls.conf").render(locals())) | ||||||
|  |             click.echo("Generated %s" % tls_config.name) | ||||||
|  |  | ||||||
|         if os.path.exists(const.SERVER_CONFIG_PATH): |         if os.path.exists(const.SERVER_CONFIG_PATH): | ||||||
|             click.echo("Configuration file %s already exists, remove to regenerate" % const.SERVER_CONFIG_PATH) |             click.echo("Configuration file %s already exists, remove to regenerate" % const.SERVER_CONFIG_PATH) | ||||||
|         else: |         else: | ||||||
| @@ -1227,6 +1256,14 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat | |||||||
|                 fh.write(env.get_template("server/builder.conf").render(vars())) |                 fh.write(env.get_template("server/builder.conf").render(vars())) | ||||||
|             click.echo("File %s created" % const.BUILDER_CONFIG_PATH) |             click.echo("File %s created" % const.BUILDER_CONFIG_PATH) | ||||||
|  |  | ||||||
|  |         # Create image builder site script | ||||||
|  |         if os.path.exists(const.BUILDER_SITE_SCRIPT): | ||||||
|  |             click.echo("Image builder site customization script %s already exists, remove to regenerate" % const.BUILDER_SITE_SCRIPT) | ||||||
|  |         else: | ||||||
|  |             with open(const.BUILDER_SITE_SCRIPT, "w") as fh: | ||||||
|  |                 fh.write(env.get_template("server/site.sh").render(vars())) | ||||||
|  |             click.echo("File %s created" % const.BUILDER_SITE_SCRIPT) | ||||||
|  |  | ||||||
|         # Create signature profile config |         # Create signature profile config | ||||||
|         if os.path.exists(const.PROFILE_CONFIG_PATH): |         if os.path.exists(const.PROFILE_CONFIG_PATH): | ||||||
|             click.echo("Signature profile config %s already exists, remove to regenerate" % const.PROFILE_CONFIG_PATH) |             click.echo("Signature profile config %s already exists, remove to regenerate" % const.PROFILE_CONFIG_PATH) | ||||||
| @@ -1235,20 +1272,16 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat | |||||||
|                 fh.write(env.get_template("server/profile.conf").render(vars())) |                 fh.write(env.get_template("server/profile.conf").render(vars())) | ||||||
|             click.echo("File %s created" % const.PROFILE_CONFIG_PATH) |             click.echo("File %s created" % const.PROFILE_CONFIG_PATH) | ||||||
|  |  | ||||||
|         if not os.path.exists("/var/lib/certidude/builder"): |  | ||||||
|             click.echo("Creating %s" % "/var/lib/certidude/builder") |  | ||||||
|             os.makedirs("/var/lib/certidude/builder") |  | ||||||
|  |  | ||||||
|         # Create subdirectories with 770 permissions |         # Create subdirectories with 770 permissions | ||||||
|         os.umask(0o007) |         os.umask(0o007) | ||||||
|         for subdir in ("signed", "signed/by-serial", "requests", "revoked", "expired", "meta"): |         for subdir in ("signed", "signed/by-serial", "requests", "revoked", "expired", "meta", "builder"): | ||||||
|             path = os.path.join(directory, subdir) |             path = os.path.join(directory, subdir) | ||||||
|             if not os.path.exists(path): |             if not os.path.exists(path): | ||||||
|                 click.echo("Creating directory %s" % path) |                 click.echo("Creating directory %s" % path) | ||||||
|                 os.mkdir(path) |                 os.mkdir(path) | ||||||
|             else: |             else: | ||||||
|                 click.echo("Directory already exists %s" % path) |                 click.echo("Directory already exists %s" % path) | ||||||
|             assert os.stat(path).st_mode == 0o40770 |             assert os.stat(path).st_mode == 0o40770, path | ||||||
|  |  | ||||||
|         # Create SQLite database file with correct permissions |         # Create SQLite database file with correct permissions | ||||||
|         os.umask(0o117) |         os.umask(0o117) | ||||||
| @@ -1293,17 +1326,15 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat | |||||||
|                 click.echo("  chmod 0644 %s" % ca_cert) |                 click.echo("  chmod 0644 %s" % ca_cert) | ||||||
|                 click.echo() |                 click.echo() | ||||||
|                 click.echo("To finish setup procedure run 'certidude setup authority' again") |                 click.echo("To finish setup procedure run 'certidude setup authority' again") | ||||||
|                 sys.exit(1) |                 sys.exit(1) # stop this fork here with error | ||||||
|  |  | ||||||
|             # https://technet.microsoft.com/en-us/library/aa998840(v=exchg.141).aspx |             # https://technet.microsoft.com/en-us/library/aa998840(v=exchg.141).aspx | ||||||
|             builder = CertificateBuilder(distinguished_name, public_key) |             builder = CertificateBuilder(distinguished_name, public_key) | ||||||
|             builder.self_signed = True |             builder.self_signed = True | ||||||
|             builder.ca = True |             builder.ca = True | ||||||
|             builder.serial_number = random.randint( |             builder.serial_number = generate_serial() | ||||||
|                 0x100000000000000000000000000000000000000, |  | ||||||
|                 0xfffffffffffffffffffffffffffffffffffffff) |  | ||||||
|  |  | ||||||
|             builder.begin_date = NOW - timedelta(minutes=5) |             builder.begin_date = NOW - const.CLOCK_SKEW_TOLERANCE | ||||||
|             builder.end_date = NOW + timedelta(days=authority_lifetime) |             builder.end_date = NOW + timedelta(days=authority_lifetime) | ||||||
|  |  | ||||||
|             certificate = builder.build(private_key) |             certificate = builder.build(private_key) | ||||||
| @@ -1312,14 +1343,18 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat | |||||||
|             os.umask(0o137) |             os.umask(0o137) | ||||||
|             with open(ca_cert, 'wb') as f: |             with open(ca_cert, 'wb') as f: | ||||||
|                 f.write(pem_armor_certificate(certificate)) |                 f.write(pem_armor_certificate(certificate)) | ||||||
|  |             click.echo("Authority certificate written to: %s" % ca_cert) | ||||||
|  |  | ||||||
|         sys.exit(0) # stop this fork here |         sys.exit(0) # stop this fork here | ||||||
|     else: |     else: | ||||||
|  |  | ||||||
|         _, exitcode = os.waitpid(bootstrap_pid, 0) |         _, exitcode = os.waitpid(bootstrap_pid, 0) | ||||||
|         if exitcode: |         if exitcode: | ||||||
|             return 0 |             return 0 | ||||||
|         from certidude import authority |         from certidude import authority | ||||||
|         authority.self_enroll(skip_notify=True) |         authority.self_enroll(skip_notify=True) | ||||||
|  |         assert os.path.exists(self_key) | ||||||
|  |         assert os.path.exists(os.path.join(directory, "signed", const.FQDN) + ".pem") | ||||||
|         assert os.getuid() == 0 and os.getgid() == 0, "Enroll contaminated environment" |         assert os.getuid() == 0 and os.getgid() == 0, "Enroll contaminated environment" | ||||||
|         assert os.stat(sqlite_path).st_mode == 0o100660 |         assert os.stat(sqlite_path).st_mode == 0o100660 | ||||||
|         assert os.stat(ca_cert).st_mode == 0o100640 |         assert os.stat(ca_cert).st_mode == 0o100640 | ||||||
| @@ -1343,6 +1378,11 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat | |||||||
|         click.echo("  openssl x509 -text -noout -in %s | less" % ca_cert) |         click.echo("  openssl x509 -text -noout -in %s | less" % ca_cert) | ||||||
|         click.echo("  openssl rsa -check -in %s" % ca_key) |         click.echo("  openssl rsa -check -in %s" % ca_key) | ||||||
|         click.echo("  openssl verify -CAfile %s %s" % (ca_cert, ca_cert)) |         click.echo("  openssl verify -CAfile %s %s" % (ca_cert, ca_cert)) | ||||||
|  |         click.echo() | ||||||
|  |         click.echo("To inspect logs and issued tokens:") | ||||||
|  |         click.echo() | ||||||
|  |         click.echo("  echo 'select * from log;' | sqlite3 /var/lib/certidude/meta/db.sqlite") | ||||||
|  |         click.echo("  echo 'select * from token;' | sqlite3 /var/lib/certidude/meta/db.sqlite") | ||||||
|         return 0 |         return 0 | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -1461,7 +1501,7 @@ def certidude_revoke(common_name, reason): | |||||||
| @click.command("expire", help="Move expired certificates") | @click.command("expire", help="Move expired certificates") | ||||||
| def certidude_expire(): | def certidude_expire(): | ||||||
|     from certidude import authority, config |     from certidude import authority, config | ||||||
|     threshold = datetime.utcnow() - timedelta(minutes=5) # Kerberos tolerance |     threshold = datetime.utcnow() - const.CLOCK_SKEW_TOLERANCE | ||||||
|     for common_name, path, buf, cert, signed, expires in authority.list_signed(): |     for common_name, path, buf, cert, signed, expires in authority.list_signed(): | ||||||
|         if expires < threshold: |         if expires < threshold: | ||||||
|             expired_path = os.path.join(config.EXPIRED_DIR, "%040x.pem" % cert.serial_number) |             expired_path = os.path.join(config.EXPIRED_DIR, "%040x.pem" % cert.serial_number) | ||||||
| @@ -1492,6 +1532,10 @@ def certidude_serve(port, listen, fork): | |||||||
|  |  | ||||||
|     from certidude import config |     from certidude import config | ||||||
|  |  | ||||||
|  |     click.echo("OCSP responder subnets: %s" % config.OCSP_SUBNETS) | ||||||
|  |     click.echo("CRL subnets: %s" % config.CRL_SUBNETS) | ||||||
|  |     click.echo("SCEP subnets: %s" % config.SCEP_SUBNETS) | ||||||
|  |  | ||||||
|     click.echo("Loading signature profiles:") |     click.echo("Loading signature profiles:") | ||||||
|     for profile in config.PROFILES.values(): |     for profile in config.PROFILES.values(): | ||||||
|          click.echo("- %s" % profile) |          click.echo("- %s" % profile) | ||||||
| @@ -1625,6 +1669,35 @@ def certidude_test(recipient): | |||||||
|         to=recipient |         to=recipient | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  | @click.command("list", help="List tokens") | ||||||
|  | def certidude_token_list(): | ||||||
|  |     from certidude import config | ||||||
|  |     from certidude.tokens import TokenManager | ||||||
|  |     token_manager = TokenManager(config.TOKEN_DATABASE) | ||||||
|  |     cols = "uuid", "expires", "subject", "state" | ||||||
|  |     now = datetime.utcnow() | ||||||
|  |     for token in token_manager.list(expired=True, used=True, token=True): | ||||||
|  |         token["state"] = "used" if token.get("used") else ("valid" if token.get("expires") > now  else "expired") | ||||||
|  |         print(";".join([str(token.get(col)) for col in cols])) | ||||||
|  |  | ||||||
|  | @click.command("purge", help="Purge tokens") | ||||||
|  | @click.option("-a", "--all", default=False, is_flag=True, help="Purge all not only expired tokens") | ||||||
|  | def certidude_token_purge(all): | ||||||
|  |     from certidude import config | ||||||
|  |     from certidude.tokens import TokenManager | ||||||
|  |     token_manager = TokenManager(config.TOKEN_DATABASE) | ||||||
|  |     print(token_manager.purge(all)) | ||||||
|  |  | ||||||
|  | @click.command("issue", help="Issue token") | ||||||
|  | @click.option("-m", "--subject-mail", default=None, help="Subject e-mail override") | ||||||
|  | @click.argument("subject") | ||||||
|  | def certidude_token_issue(subject, subject_mail): | ||||||
|  |     from certidude import config | ||||||
|  |     from certidude.tokens import TokenManager | ||||||
|  |     from certidude.user import User | ||||||
|  |     token_manager = TokenManager(config.TOKEN_DATABASE) | ||||||
|  |     token_manager.issue(None, User.objects.get(subject), subject_mail) | ||||||
|  |  | ||||||
|  |  | ||||||
| @click.group("strongswan", help="strongSwan helpers") | @click.group("strongswan", help="strongSwan helpers") | ||||||
| def certidude_setup_strongswan(): pass | def certidude_setup_strongswan(): pass | ||||||
| @@ -1635,6 +1708,9 @@ 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("token", help="Token management") | ||||||
|  | def certidude_token(): pass | ||||||
|  |  | ||||||
| @click.group() | @click.group() | ||||||
| def entry_point(): pass | def entry_point(): pass | ||||||
|  |  | ||||||
| @@ -1649,6 +1725,10 @@ 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_nginx) | certidude_setup.add_command(certidude_setup_nginx) | ||||||
| certidude_setup.add_command(certidude_setup_yubikey) | certidude_setup.add_command(certidude_setup_yubikey) | ||||||
|  | certidude_token.add_command(certidude_token_list) | ||||||
|  | certidude_token.add_command(certidude_token_purge) | ||||||
|  | certidude_token.add_command(certidude_token_issue) | ||||||
|  | entry_point.add_command(certidude_token) | ||||||
| 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_enroll) | entry_point.add_command(certidude_enroll) | ||||||
|   | |||||||
| @@ -2,6 +2,16 @@ | |||||||
| import os | import os | ||||||
| import click | import click | ||||||
| import subprocess | import subprocess | ||||||
|  | from random import SystemRandom | ||||||
|  |  | ||||||
|  | random = SystemRandom() | ||||||
|  |  | ||||||
|  | try: | ||||||
|  |     from time import time_ns | ||||||
|  | except ImportError: | ||||||
|  |     from time import time | ||||||
|  |     def time_ns(): | ||||||
|  |         return int(time() * 10**9) # 64 bits integer, 32 ns bits | ||||||
|  |  | ||||||
| MAPPING = dict( | MAPPING = dict( | ||||||
|     common_name="CN", |     common_name="CN", | ||||||
| @@ -122,3 +132,6 @@ def pip(packages): | |||||||
|     pip.main(['install'] + packages.split(" ")) |     pip.main(['install'] + packages.split(" ")) | ||||||
|     return True |     return True | ||||||
|  |  | ||||||
|  | def generate_serial(): | ||||||
|  |     return time_ns() << 56 | random.randint(0, 2**56-1) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -23,6 +23,7 @@ LDAP_AUTHENTICATION_URI = cp.get("authentication", "ldap uri") | |||||||
| LDAP_GSSAPI_CRED_CACHE = cp.get("accounts", "ldap gssapi credential cache") | LDAP_GSSAPI_CRED_CACHE = cp.get("accounts", "ldap gssapi credential cache") | ||||||
| LDAP_ACCOUNTS_URI = cp.get("accounts", "ldap uri") | LDAP_ACCOUNTS_URI = cp.get("accounts", "ldap uri") | ||||||
| LDAP_BASE = cp.get("accounts", "ldap base") | LDAP_BASE = cp.get("accounts", "ldap base") | ||||||
|  | LDAP_MAIL_ATTRIBUTE = cp.get("accounts", "ldap mail attribute") | ||||||
|  |  | ||||||
| USER_SUBNETS = set([ipaddress.ip_network(j) for j in | USER_SUBNETS = set([ipaddress.ip_network(j) for j in | ||||||
|     cp.get("authorization", "user subnets").split(" ") if j]) |     cp.get("authorization", "user subnets").split(" ") if j]) | ||||||
| @@ -71,8 +72,7 @@ USER_MULTIPLE_CERTIFICATES = { | |||||||
|  |  | ||||||
| REQUEST_SUBMISSION_ALLOWED = cp.getboolean("authority", "request submission allowed") | REQUEST_SUBMISSION_ALLOWED = cp.getboolean("authority", "request submission allowed") | ||||||
| AUTHORITY_CERTIFICATE_URL = cp.get("signature", "authority certificate url") | AUTHORITY_CERTIFICATE_URL = cp.get("signature", "authority certificate url") | ||||||
| AUTHORITY_CRL_URL = cp.get("signature", "revoked url") | AUTHORITY_CRL_URL = "http://%s/api/revoked" % const.FQDN | ||||||
| AUTHORITY_OCSP_URL = cp.get("signature", "responder url") |  | ||||||
|  |  | ||||||
| REVOCATION_LIST_LIFETIME = cp.getint("signature", "revocation list lifetime") | REVOCATION_LIST_LIFETIME = cp.getint("signature", "revocation list lifetime") | ||||||
|  |  | ||||||
| @@ -88,6 +88,8 @@ USERS_GROUP = cp.get("authorization", "posix user group") | |||||||
| ADMIN_GROUP = cp.get("authorization", "posix admin group") | ADMIN_GROUP = cp.get("authorization", "posix admin group") | ||||||
| LDAP_USER_FILTER = cp.get("authorization", "ldap user filter") | LDAP_USER_FILTER = cp.get("authorization", "ldap user filter") | ||||||
| LDAP_ADMIN_FILTER = cp.get("authorization", "ldap admin filter") | LDAP_ADMIN_FILTER = cp.get("authorization", "ldap admin filter") | ||||||
|  | LDAP_COMPUTER_FILTER = cp.get("authorization", "ldap computer filter") | ||||||
|  |  | ||||||
| if "%s" not in LDAP_USER_FILTER: raise ValueError("No placeholder %s for username in 'ldap user filter'") | if "%s" not in LDAP_USER_FILTER: raise ValueError("No placeholder %s for username in 'ldap user filter'") | ||||||
| if "%s" not in LDAP_ADMIN_FILTER: raise ValueError("No placeholder %s for username in 'ldap admin filter'") | if "%s" not in LDAP_ADMIN_FILTER: raise ValueError("No placeholder %s for username in 'ldap admin filter'") | ||||||
|  |  | ||||||
| @@ -95,9 +97,9 @@ TAG_TYPES = [j.split("/", 1) + [cp.get("tagging", j)] for j in cp.options("taggi | |||||||
|  |  | ||||||
| # Tokens | # Tokens | ||||||
| TOKEN_URL = cp.get("token", "url") | TOKEN_URL = cp.get("token", "url") | ||||||
| TOKEN_LIFETIME = cp.getint("token", "lifetime") * 60 # Convert minutes to seconds | TOKEN_BACKEND = cp.get("token", "backend") | ||||||
| TOKEN_SECRET = cp.get("token", "secret").encode("ascii") | TOKEN_LIFETIME = timedelta(minutes=cp.getint("token", "lifetime")) # Convert minutes to seconds | ||||||
|  | TOKEN_DATABASE = cp.get("token", "database") | ||||||
| # TODO: Check if we don't have base or servers | # TODO: Check if we don't have base or servers | ||||||
|  |  | ||||||
| # The API call for looking up scripts uses following directory as root | # The API call for looking up scripts uses following directory as root | ||||||
| @@ -115,11 +117,13 @@ PROFILES = dict([(key, SignatureProfile(key, | |||||||
|     profile_config.get(key, "key usage"), |     profile_config.get(key, "key usage"), | ||||||
|     profile_config.get(key, "extended key usage"), |     profile_config.get(key, "extended key usage"), | ||||||
|     profile_config.get(key, "common name"), |     profile_config.get(key, "common name"), | ||||||
|     )) for key in profile_config.sections()]) |     profile_config.get(key,  "revoked url"), | ||||||
|  |     profile_config.get(key, "responder url") | ||||||
|  | )) for key in profile_config.sections() if profile_config.getboolean(key, "enabled")]) | ||||||
|  |  | ||||||
| cp2 = configparser.RawConfigParser() | cp2 = configparser.RawConfigParser() | ||||||
| cp2.readfp(open(const.BUILDER_CONFIG_PATH, "r")) | cp2.readfp(open(const.BUILDER_CONFIG_PATH, "r")) | ||||||
| IMAGE_BUILDER_PROFILES = [(j, cp2.get(j, "title"), cp2.get(j, "rename")) for j in cp2.sections()] | IMAGE_BUILDER_PROFILES = [(j, cp2.get(j, "title"), cp2.get(j, "rename")) for j in cp2.sections() if cp2.getboolean(j, "enabled")] | ||||||
|  |  | ||||||
| TOKEN_OVERWRITE_PERMITTED=True | TOKEN_OVERWRITE_PERMITTED=True | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,17 +3,21 @@ import click | |||||||
| import os | import os | ||||||
| import socket | import socket | ||||||
| import sys | import sys | ||||||
|  | from datetime import timedelta | ||||||
|  |  | ||||||
| KEY_SIZE = 1024 if os.getenv("TRAVIS") else 4096 | KEY_SIZE = 1024 if os.getenv("TRAVIS") else 4096 | ||||||
| CURVE_NAME = "secp384r1" | CURVE_NAME = "secp384r1" | ||||||
| RE_FQDN =  "^(([a-z0-9]|[a-z0-9][a-z0-9\-_]*[a-z0-9])\.)+([a-z0-9]|[a-z0-9][a-z0-9\-_]*[a-z0-9])?$" | RE_FQDN =  "^(([a-z0-9]|[a-z0-9][a-z0-9\-_]*[a-z0-9])\.)+([a-z0-9]|[a-z0-9][a-z0-9\-_]*[a-z0-9])?$" | ||||||
| RE_HOSTNAME =  "^[a-z0-9]([a-z0-9\-_]{0,61}[a-z0-9])?$" | RE_HOSTNAME =  "^[a-z0-9]([a-z0-9\-_]{0,61}[a-z0-9])?$" | ||||||
| RE_COMMON_NAME = "^[A-Za-z0-9\-\.\_@]+$" | RE_COMMON_NAME = "^[A-Za-z0-9\-\.\_@]+$" | ||||||
|  | CLOCK_SKEW_TOLERANCE = timedelta(minutes=5) # Kerberos-like clock skew tolerance | ||||||
|  |  | ||||||
| RUN_DIR = "/run/certidude" | RUN_DIR = "/run/certidude" | ||||||
| CONFIG_DIR = "/etc/certidude" | CONFIG_DIR = "/etc/certidude" | ||||||
| SERVER_CONFIG_PATH = os.path.join(CONFIG_DIR, "server.conf") | SERVER_CONFIG_PATH = os.path.join(CONFIG_DIR, "server.conf") | ||||||
| BUILDER_CONFIG_PATH = os.path.join(CONFIG_DIR, "builder.conf") | BUILDER_CONFIG_PATH = os.path.join(CONFIG_DIR, "builder.conf") | ||||||
|  | SCRIPT_DIR = os.path.join(CONFIG_DIR, "script") | ||||||
|  | BUILDER_SITE_SCRIPT = os.path.join(SCRIPT_DIR, "site.sh") | ||||||
| PROFILE_CONFIG_PATH = os.path.join(CONFIG_DIR, "profile.conf") | PROFILE_CONFIG_PATH = os.path.join(CONFIG_DIR, "profile.conf") | ||||||
| CLIENT_CONFIG_PATH = os.path.join(CONFIG_DIR, "client.conf") | CLIENT_CONFIG_PATH = os.path.join(CONFIG_DIR, "client.conf") | ||||||
| SERVICES_CONFIG_PATH = os.path.join(CONFIG_DIR, "services.conf") | SERVICES_CONFIG_PATH = os.path.join(CONFIG_DIR, "services.conf") | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ from datetime import timedelta | |||||||
| from certidude import const | from certidude import const | ||||||
|  |  | ||||||
| class SignatureProfile(object): | class SignatureProfile(object): | ||||||
|     def __init__(self, slug, title, ou, ca, lifetime, key_usage, extended_key_usage, common_name): |     def __init__(self, slug, title, ou, ca, lifetime, key_usage, extended_key_usage, common_name, revoked_url, responder_url): | ||||||
|         self.slug = slug |         self.slug = slug | ||||||
|         self.title = title |         self.title = title | ||||||
|         self.ou = ou or None |         self.ou = ou or None | ||||||
| @@ -12,6 +12,9 @@ class SignatureProfile(object): | |||||||
|         self.lifetime = lifetime |         self.lifetime = lifetime | ||||||
|         self.key_usage = set(key_usage.split(" ")) if key_usage else set() |         self.key_usage = set(key_usage.split(" ")) if key_usage else set() | ||||||
|         self.extended_key_usage = set(extended_key_usage.split(" ")) if extended_key_usage else set() |         self.extended_key_usage = set(extended_key_usage.split(" ")) if extended_key_usage else set() | ||||||
|  |         self.responder_url = responder_url | ||||||
|  |         self.revoked_url = revoked_url | ||||||
|  |  | ||||||
|         if common_name.startswith("^"): |         if common_name.startswith("^"): | ||||||
|             self.common_name = common_name |             self.common_name = common_name | ||||||
|         elif common_name == "RE_HOSTNAME": |         elif common_name == "RE_HOSTNAME": | ||||||
| @@ -39,7 +42,7 @@ class SignatureProfile(object): | |||||||
|  |  | ||||||
|     def serialize(self): |     def serialize(self): | ||||||
|         return dict([(key, getattr(self,key)) for key in ( |         return dict([(key, getattr(self,key)) for key in ( | ||||||
|             "slug", "title", "ou", "ca", "lifetime", "key_usage", "extended_key_usage", "common_name")]) |             "slug", "title", "ou", "ca", "lifetime", "key_usage", "extended_key_usage", "common_name", "responder_url", "revoked_url")]) | ||||||
|  |  | ||||||
|     def __repr__(self): |     def __repr__(self): | ||||||
|         bits = [] |         bits = [] | ||||||
| @@ -47,6 +50,10 @@ class SignatureProfile(object): | |||||||
|             bits.append("%d years" % (self.lifetime / 365)) |             bits.append("%d years" % (self.lifetime / 365)) | ||||||
|         if self.lifetime % 365: |         if self.lifetime % 365: | ||||||
|             bits.append("%d days" % (self.lifetime % 365)) |             bits.append("%d days" % (self.lifetime % 365)) | ||||||
|         return "%s (title=%s, ca=%s, ou=%s, lifetime=%s, key_usage=%s, extended_key_usage=%s, common_name=%s)" % ( |         return "%s (title=%s, ca=%s, ou=%s, lifetime=%s, key_usage=%s, extended_key_usage=%s, common_name=%s, responder_url=%s, revoked_url=%s)" % ( | ||||||
|             self.slug, self.title, self.ca, self.ou, " ".join(bits), self.key_usage, self.extended_key_usage, self.common_name) |             self.slug, self.title, self.ca, self.ou, " ".join(bits), | ||||||
|  |             self.key_usage, self.extended_key_usage, | ||||||
|  |             repr(self.common_name), | ||||||
|  |             repr(self.responder_url), | ||||||
|  |             repr(self.revoked_url)) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -14,6 +14,9 @@ class RelationalMixin(object): | |||||||
|  |  | ||||||
|     SQL_CREATE_TABLES = "" |     SQL_CREATE_TABLES = "" | ||||||
|  |  | ||||||
|  |     class DoesNotExist(Exception): | ||||||
|  |         pass | ||||||
|  |  | ||||||
|     def __init__(self, uri): |     def __init__(self, uri): | ||||||
|         self.uri = urlparse(uri) |         self.uri = urlparse(uri) | ||||||
|  |  | ||||||
| @@ -29,7 +32,8 @@ class RelationalMixin(object): | |||||||
|             if self.uri.netloc: |             if self.uri.netloc: | ||||||
|                 raise ValueError("Malformed database URI %s" % self.uri) |                 raise ValueError("Malformed database URI %s" % self.uri) | ||||||
|             import sqlite3 |             import sqlite3 | ||||||
|             conn = sqlite3.connect(self.uri.path) |             conn = sqlite3.connect(self.uri.path, | ||||||
|  |                 detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES) | ||||||
|         else: |         else: | ||||||
|             raise NotImplementedError("Unsupported database scheme %s, currently only mysql://user:pass@host/database or sqlite:///path/to/database.sqlite is supported" % o.scheme) |             raise NotImplementedError("Unsupported database scheme %s, currently only mysql://user:pass@host/database or sqlite:///path/to/database.sqlite is supported" % o.scheme) | ||||||
|  |  | ||||||
| @@ -74,7 +78,6 @@ class RelationalMixin(object): | |||||||
|         conn.close() |         conn.close() | ||||||
|         return rowid |         return rowid | ||||||
|  |  | ||||||
|  |  | ||||||
|     def iterfetch(self, query, *args): |     def iterfetch(self, query, *args): | ||||||
|         conn = self.sql_connect() |         conn = self.sql_connect() | ||||||
|         cursor = conn.cursor() |         cursor = conn.cursor() | ||||||
| @@ -86,3 +89,24 @@ class RelationalMixin(object): | |||||||
|             cursor.close() |             cursor.close() | ||||||
|             conn.close() |             conn.close() | ||||||
|         return tuple(g()) |         return tuple(g()) | ||||||
|  |  | ||||||
|  |     def get(self, query, *args): | ||||||
|  |         conn = self.sql_connect() | ||||||
|  |         cursor = conn.cursor() | ||||||
|  |         cursor.execute(query, args) | ||||||
|  |         row = cursor.fetchone() | ||||||
|  |         cursor.close() | ||||||
|  |         conn.close() | ||||||
|  |         if not row: | ||||||
|  |             raise self.DoesNotExist("No matches for query '%s' with parameters %s" % (query, repr(args))) | ||||||
|  |         return row | ||||||
|  |  | ||||||
|  |     def execute(self, query, *args): | ||||||
|  |         conn = self.sql_connect() | ||||||
|  |         cursor = conn.cursor() | ||||||
|  |         cursor.execute(query, args) | ||||||
|  |         affected_rows = cursor.rowcount | ||||||
|  |         cursor.close() | ||||||
|  |         conn.commit() | ||||||
|  |         conn.close() | ||||||
|  |         return affected_rows | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| create table if not exists log ( | create table if not exists log ( | ||||||
|  |     id integer primary key autoincrement, | ||||||
|     created datetime, |     created datetime, | ||||||
|     facility varchar(30), |     facility varchar(30), | ||||||
|     level int, |     level int, | ||||||
|   | |||||||
							
								
								
									
										17
									
								
								certidude/sql/sqlite/token_issue.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,17 @@ | |||||||
|  | insert into token ( | ||||||
|  |     created, | ||||||
|  |     expires, | ||||||
|  |     uuid, | ||||||
|  |     issuer, | ||||||
|  |     subject, | ||||||
|  |     mail, | ||||||
|  |     profile | ||||||
|  | ) values ( | ||||||
|  |     ?, | ||||||
|  |     ?, | ||||||
|  |     ?, | ||||||
|  |     ?, | ||||||
|  |     ?, | ||||||
|  |     ?, | ||||||
|  |     ? | ||||||
|  | ); | ||||||
							
								
								
									
										13
									
								
								certidude/sql/sqlite/token_tables.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,13 @@ | |||||||
|  | create table if not exists token ( | ||||||
|  |     id integer primary key autoincrement, | ||||||
|  |     created datetime, | ||||||
|  |     used datetime, | ||||||
|  |     expires datetime, | ||||||
|  |     uuid char(32), | ||||||
|  |     issuer char(30), | ||||||
|  |     subject varchar(30), | ||||||
|  |     mail varchar(128), | ||||||
|  |     profile varchar(10), | ||||||
|  |  | ||||||
|  |     constraint unique_uuid unique(uuid) | ||||||
|  | ) | ||||||
							
								
								
									
										
											BIN
										
									
								
								certidude/static/img/ubuntu-01-edit-connections.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 393 KiB | 
							
								
								
									
										
											BIN
										
									
								
								certidude/static/img/ubuntu-02-network-connections.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 368 KiB | 
							
								
								
									
										
											BIN
										
									
								
								certidude/static/img/ubuntu-03-import-saved-config.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 328 KiB | 
							
								
								
									
										
											BIN
										
									
								
								certidude/static/img/ubuntu-04-select-file.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 139 KiB | 
							
								
								
									
										
											BIN
										
									
								
								certidude/static/img/ubuntu-05-profile-imported.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 338 KiB | 
							
								
								
									
										
											BIN
										
									
								
								certidude/static/img/ubuntu-06-ipv4-settings.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 318 KiB | 
							
								
								
									
										
											BIN
										
									
								
								certidude/static/img/ubuntu-07-disable-default-route.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 342 KiB | 
							
								
								
									
										
											BIN
										
									
								
								certidude/static/img/ubuntu-08-activate-connection.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 394 KiB | 
							
								
								
									
										
											BIN
										
									
								
								certidude/static/img/windows-01-download-openvpn.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 132 KiB | 
							
								
								
									
										
											BIN
										
									
								
								certidude/static/img/windows-02-install-openvpn.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 502 KiB | 
							
								
								
									
										
											BIN
										
									
								
								certidude/static/img/windows-03-move-config-file.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 137 KiB | 
							
								
								
									
										
											BIN
										
									
								
								certidude/static/img/windows-04-connect.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 583 KiB | 
							
								
								
									
										
											BIN
										
									
								
								certidude/static/img/windows-05-connected.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 638 KiB | 
| @@ -20,20 +20,14 @@ | |||||||
|       <div class="collapse navbar-collapse" id="navbarsExampleDefault"> |       <div class="collapse navbar-collapse" id="navbarsExampleDefault"> | ||||||
|         <ul class="navbar-nav mr-auto"> |         <ul class="navbar-nav mr-auto"> | ||||||
|           <li class="nav-item"> |           <li class="nav-item"> | ||||||
|             <a class="nav-link disabled dashboard" href="#columns=2">Dashboard</a> |             <a class="nav-link disabled dashboard" href="#">Dashboard</a> | ||||||
|           </li> |           </li> | ||||||
|           <li class="nav-item"> |           <li class="nav-item hidden-xl-up"> | ||||||
|             <a class="nav-link" href="#columns=3">Wider</a> |  | ||||||
|           </li> |  | ||||||
|           <li class="nav-item"> |  | ||||||
|             <a class="nav-link" href="#columns=4">Widest</a> |  | ||||||
|           </li> |  | ||||||
|           <li class="nav-item"> |  | ||||||
|             <a class="nav-link" href="#">Log</a> |             <a class="nav-link" href="#">Log</a> | ||||||
|           </li> |           </li> | ||||||
|         </ul> |         </ul> | ||||||
|         <form class="form-inline my-2 my-lg-0"> |         <form class="form-inline my-2 my-lg-0"> | ||||||
|           <input id="search" class="form-control mr-sm-2" type="search" placeholder="🔍"> |           <input id="search" class="form-control mr-sm-2" style="display:none;" type="search" placeholder="🔍"> | ||||||
|         </form> |         </form> | ||||||
|       </div> |       </div> | ||||||
|     </nav> |     </nav> | ||||||
|   | |||||||
| @@ -1,15 +1,8 @@ | |||||||
|  |  | ||||||
| 'use strict'; | 'use strict'; | ||||||
|  |  | ||||||
| const KEYWORDS = [ | const KEY_SIZE = 2048; | ||||||
|     ["Android", "android"], | const DEVICE_KEYWORDS = ["Android", "iPhone", "iPad", "Windows", "Ubuntu", "Fedora", "Mac", "Linux"]; | ||||||
|     ["iPhone", "iphone"], |  | ||||||
|     ["iPad", "ipad"], |  | ||||||
|     ["Ubuntu", "ubuntu"], |  | ||||||
|     ["Fedora", "fedora"], |  | ||||||
|     ["Linux", "linux"], |  | ||||||
|     ["Macintosh", "mac"], |  | ||||||
| ]; |  | ||||||
|  |  | ||||||
| jQuery.timeago.settings.allowFuture = true; | jQuery.timeago.settings.allowFuture = true; | ||||||
|  |  | ||||||
| @@ -17,38 +10,217 @@ function normalizeCommonName(j) { | |||||||
|     return j.replace("@", "--").split(".").join("-"); // dafuq ?! |     return j.replace("@", "--").split(".").join("-"); // dafuq ?! | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function onShowAll() { | ||||||
|  |   var options = document.querySelectorAll(".option"); | ||||||
|  |   for (i = 0; i < options.length; i++) { | ||||||
|  |       options[i].style.display = "block"; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function onKeyGen() { | ||||||
|  |   if (window.navigator.userAgent.indexOf(" Edge/") >= 0) { | ||||||
|  |     $("#enroll .loader-container").hide(); | ||||||
|  |     $("#enroll .edge-broken").show(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   window.keys = forge.pki.rsa.generateKeyPair(KEY_SIZE); | ||||||
|  |   console.info('Key-pair created.'); | ||||||
|  |  | ||||||
|  |   // Device identifier | ||||||
|  |   var dig = forge.md.sha384.create(); | ||||||
|  |   dig.update(window.navigator.userAgent); | ||||||
|  |  | ||||||
|  |   var prefix = "unknown"; | ||||||
|  |   for (i in DEVICE_KEYWORDS) { | ||||||
|  |     var keyword = DEVICE_KEYWORDS[i]; | ||||||
|  |     if (window.navigator.userAgent.indexOf(keyword) >= 0) { | ||||||
|  |       prefix = keyword.toLowerCase(); | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   window.identifier = prefix + "-" + dig.digest().toHex().substring(0, 8); | ||||||
|  |   console.info("Device identifier:", identifier); | ||||||
|  |  | ||||||
|  |   window.common_name = query.subject + "@" + identifier; | ||||||
|  |  | ||||||
|  |   window.csr = forge.pki.createCertificationRequest(); | ||||||
|  |   csr.publicKey = keys.publicKey; | ||||||
|  |   csr.setSubject([{ | ||||||
|  |     name: 'commonName', value: common_name | ||||||
|  |   }]); | ||||||
|  |  | ||||||
|  |   csr.sign(keys.privateKey, forge.md.sha384.create()); | ||||||
|  |   console.info('Certification request created'); | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   $("#enroll .loader-container").hide(); | ||||||
|  |  | ||||||
|  |   var prefix = null; | ||||||
|  |   for (i in DEVICE_KEYWORDS) { | ||||||
|  |     var keyword = DEVICE_KEYWORDS[i]; | ||||||
|  |     if (window.navigator.userAgent.indexOf(keyword) >= 0) { | ||||||
|  |       prefix = keyword.toLowerCase(); | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (prefix == null) { | ||||||
|  |       $(".option").show(); | ||||||
|  |       return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   var protocols = query.protocols.split(","); | ||||||
|  |   console.info("Showing snippets for:", protocols); | ||||||
|  |   for (var j = 0; j < protocols.length; j++) { | ||||||
|  |       var options = document.querySelectorAll(".option." + protocols[j] + "." + prefix); | ||||||
|  |       for (i = 0; i < options.length; i++) { | ||||||
|  |           options[i].style.display = "block"; | ||||||
|  |       } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function onEnroll(encoding) { | ||||||
|  |   console.info("User agent:", window.navigator.userAgent); | ||||||
|  |   var xhr = new XMLHttpRequest(); | ||||||
|  |   xhr.open('GET', "/api/certificate"); | ||||||
|  |   xhr.onload = function() { | ||||||
|  |     if (xhr.status === 200) { | ||||||
|  |       var ca = forge.pki.certificateFromPem(xhr.responseText); | ||||||
|  |       console.info("Got CA certificate:"); | ||||||
|  |       var xhr2 = new XMLHttpRequest(); | ||||||
|  |       xhr2.open("PUT", "/api/token/?token=" + query.token ); | ||||||
|  |       xhr2.onload = function() { | ||||||
|  |         if (xhr2.status === 200) { | ||||||
|  |           var a = document.createElement("a"); | ||||||
|  |           var cert = forge.pki.certificateFromPem(xhr2.responseText); | ||||||
|  |           console.info("Got signed certificate:", xhr2.responseText); | ||||||
|  |           var p12 = forge.pkcs12.toPkcs12Asn1( | ||||||
|  |             keys.privateKey, [cert, ca], "", {algorithm: '3des'}); | ||||||
|  |  | ||||||
|  |           switch(encoding) { | ||||||
|  |             case 'p12': | ||||||
|  |               var buf = forge.asn1.toDer(p12).getBytes(); | ||||||
|  |               var mimetype = "application/x-pkcs12" | ||||||
|  |               a.download = query.router + ".p12"; | ||||||
|  |               break | ||||||
|  |             case 'sswan': | ||||||
|  |               var buf = JSON.stringify({ | ||||||
|  |                   uuid: "a061d140-d3f9-4db7-b2f8-32d6703f4618", | ||||||
|  |                   name: identifier, | ||||||
|  |                   type: "ikev2-cert", | ||||||
|  |                   'ike-proposal': 'aes256-sha384-prfsha384-modp2048', | ||||||
|  |                   'esp-proposal': 'aes128gcm16-aes128gmac-modp2048', | ||||||
|  |                   remote: { addr: query.router }, | ||||||
|  |                   local: { p12: forge.util.encode64(forge.asn1.toDer(p12).getBytes()) } | ||||||
|  |               }); | ||||||
|  |               console.info("Buf is:", buf); | ||||||
|  |               var mimetype = "application/vnd.strongswan.profile" | ||||||
|  |               a.download = query.router + ".sswan"; | ||||||
|  |               break | ||||||
|  |             case 'ovpn': | ||||||
|  |               var buf = nunjucks.render('snippets/openvpn-client.conf', { | ||||||
|  |                   session: { | ||||||
|  |                       authority: { | ||||||
|  |                           certificate: { | ||||||
|  |                               common_name: "Certidude at " + window.location.hostname, | ||||||
|  |                               algorithm: "rsa" | ||||||
|  |                           } | ||||||
|  |                       }, | ||||||
|  |                       service: { | ||||||
|  |                           protocols: query.protocols.split(","), | ||||||
|  |                           routers: [query.router], | ||||||
|  |                       } | ||||||
|  |                   }, | ||||||
|  |                   key: forge.pki.privateKeyToPem(keys.privateKey), | ||||||
|  |                   cert: xhr2.responseText, | ||||||
|  |                   ca: xhr.responseText | ||||||
|  |               }); | ||||||
|  |               var mimetype = "application/x-openvpn-profile"; | ||||||
|  |               a.download = query.router + ".ovpn"; | ||||||
|  |               break | ||||||
|  |             case 'mobileconfig': | ||||||
|  |               var p12 = forge.pkcs12.toPkcs12Asn1( | ||||||
|  |                   keys.privateKey, [cert, ca], "1234", {algorithm: '3des'}); | ||||||
|  |               var buf = nunjucks.render('snippets/ios.mobileconfig', { | ||||||
|  |                   session: { | ||||||
|  |                       authority: { | ||||||
|  |                           certificate: { | ||||||
|  |                               common_name: "Certidude at " + window.location.hostname, | ||||||
|  |                               algorithm: "rsa" | ||||||
|  |                           } | ||||||
|  |                       } | ||||||
|  |                   }, | ||||||
|  |                   common_name: common_name, | ||||||
|  |                   gateway: query.router, | ||||||
|  |                   p12: forge.util.encode64(forge.asn1.toDer(p12).getBytes()), | ||||||
|  |                   ca: forge.util.encode64(forge.asn1.toDer(forge.pki.certificateToAsn1(ca)).getBytes()) | ||||||
|  |               }); | ||||||
|  |               var mimetype = "application/x-apple-aspen-config"; | ||||||
|  |               a.download = query.router + ".mobileconfig"; | ||||||
|  |               break | ||||||
|  |           } | ||||||
|  |           a.href = "data:" + mimetype + ";base64," + forge.util.encode64(buf); | ||||||
|  |           console.info("Offering bundle for download"); | ||||||
|  |           document.body.appendChild(a); // Firefox needs this! | ||||||
|  |           a.click(); | ||||||
|  |         } else { | ||||||
|  |           if (xhr2.status == 403) { alert("Token used or expired"); } | ||||||
|  |           console.info('Request failed.  Returned status of ' + xhr2.status); | ||||||
|  |           try { | ||||||
|  |             var r = JSON.parse(xhr2.responseText); | ||||||
|  |             console.info("Server said: " + r.title); | ||||||
|  |             console.info(r.description); | ||||||
|  |           } catch(e) { | ||||||
|  |              console.info("Server said: " + xhr2.statusText); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       }; | ||||||
|  |       xhr2.send(forge.pki.certificationRequestToPem(csr)); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   xhr.send(); | ||||||
|  | } | ||||||
|  |  | ||||||
| function onHashChanged() { | function onHashChanged() { | ||||||
|     var query = {}; |     window.query = {}; | ||||||
|     var a = location.hash.substring(1).split('&'); |     var a = location.hash.substring(1).split('&'); | ||||||
|     for (var i = 0; i < a.length; i++) { |     for (var i = 0; i < a.length; i++) { | ||||||
|         var b = a[i].split('='); |         var b = a[i].split('='); | ||||||
|         query[decodeURIComponent(b[0])] = decodeURIComponent(b[1] || ''); |         query[decodeURIComponent(b[0])] = decodeURIComponent(b[1] || ''); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (query.columns) { query.columns = parseInt(query.columns) }; |  | ||||||
|  |  | ||||||
|     if (query.columns < 2 || query.columns > 4) { |  | ||||||
|         query.columns = 2; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     console.info("Hash is now:", query); |     console.info("Hash is now:", query); | ||||||
|  |  | ||||||
|     if (window.location.protocol != "https:") { |     if (window.location.protocol != "https:") { | ||||||
|         $.get("/api/certificate/", function(blob) { |         $.get("/api/certificate/", function(blob) { | ||||||
|             $("#view-dashboard").html(env.render('views/insecure.html', { window: window, |             $("#view-dashboard").html(env.render('views/insecure.html', { window: window, | ||||||
|                 authority_name: window.location.hostname, |                 session: { authority: { | ||||||
|                 session: { authority: { certificate: { blob: blob }}} |                     hostname: window.location.hostname, | ||||||
|  |                     certificate: { blob: blob }}} | ||||||
|             })); |             })); | ||||||
|         }); |         }); | ||||||
|  |     } else { | ||||||
|  |         if (query.action == "enroll") { | ||||||
|  |             $("#view-dashboard").html(env.render('views/enroll.html')); | ||||||
|  |             var options = document.querySelectorAll(".option"); | ||||||
|  |             for (i = 0; i < options.length; i++) { | ||||||
|  |                 options[i].style.display = "none"; | ||||||
|  |             } | ||||||
|  |             setTimeout(onKeyGen, 100); | ||||||
|  |             console.info("Generating key pair..."); | ||||||
|         } else { |         } else { | ||||||
|             loadAuthority(query); |             loadAuthority(query); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| function onTagClicked(tag) { | function onTagClicked(e) { | ||||||
|     var cn = $(tag).attr("data-cn"); |     e.preventDefault(); | ||||||
|     var id = $(tag).attr("title"); |     var cn = $(e.target).attr("data-cn"); | ||||||
|     var value = $(tag).html(); |     var id = $(e.target).attr("title"); | ||||||
|  |     var value = $(e.target).html(); | ||||||
|     var updated = prompt("Enter new tag or clear to remove the tag", value); |     var updated = prompt("Enter new tag or clear to remove the tag", value); | ||||||
|     if (updated == "") { |     if (updated == "") { | ||||||
|         $(event.target).addClass("disabled"); |         $(event.target).addClass("disabled"); | ||||||
| @@ -57,7 +229,7 @@ function onTagClicked(tag) { | |||||||
|             url: "/api/signed/" + cn + "/tag/" + id + "/" |             url: "/api/signed/" + cn + "/tag/" + id + "/" | ||||||
|         }); |         }); | ||||||
|     } else if (updated && updated != value) { |     } else if (updated && updated != value) { | ||||||
|         $(tag).addClass("disabled"); |         $(e.target).addClass("disabled"); | ||||||
|         $.ajax({ |         $.ajax({ | ||||||
|             method: "PUT", |             method: "PUT", | ||||||
|             url: "/api/signed/" + cn + "/tag/" + id + "/", |             url: "/api/signed/" + cn + "/tag/" + id + "/", | ||||||
| @@ -77,9 +249,10 @@ function onTagClicked(tag) { | |||||||
|     return false; |     return false; | ||||||
| } | } | ||||||
|  |  | ||||||
| function onNewTagClicked(menu) { | function onNewTagClicked(e) { | ||||||
|     var cn = $(menu).attr("data-cn"); |     e.preventDefault(); | ||||||
|     var key = $(menu).attr("data-key"); |     var cn = $(e.target).attr("data-cn"); | ||||||
|  |     var key = $(e.target).attr("data-key"); | ||||||
|     var value = prompt("Enter new " + key + " tag for " + cn); |     var value = prompt("Enter new " + key + " tag for " + cn); | ||||||
|     if (!value) return; |     if (!value) return; | ||||||
|     if (value.length == 0) return; |     if (value.length == 0) return; | ||||||
| @@ -101,6 +274,7 @@ function onNewTagClicked(menu) { | |||||||
|             alert(e); |             alert(e); | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
|  |     return false; | ||||||
| } | } | ||||||
|  |  | ||||||
| function onTagFilterChanged() { | function onTagFilterChanged() { | ||||||
| @@ -121,6 +295,7 @@ function onLogEntry (e) { | |||||||
|                 message: e.message, |                 message: e.message, | ||||||
|                 severity: e.severity, |                 severity: e.severity, | ||||||
|                 fresh: e.fresh, |                 fresh: e.fresh, | ||||||
|  |                 keywords: e.message.toLowerCase().split(/,?[ <>/]+/).join("|") | ||||||
|             } |             } | ||||||
|         })); |         })); | ||||||
|     } |     } | ||||||
| @@ -270,7 +445,7 @@ function onServerStopped() { | |||||||
|  |  | ||||||
| } | } | ||||||
|  |  | ||||||
| function onSendToken() { | function onIssueToken() { | ||||||
|     $.ajax({ |     $.ajax({ | ||||||
|         method: "POST", |         method: "POST", | ||||||
|         url: "/api/token/", |         url: "/api/token/", | ||||||
| @@ -316,20 +491,16 @@ function loadAuthority(query) { | |||||||
|  |  | ||||||
|             console.info("Loaded:", session); |             console.info("Loaded:", session); | ||||||
|             $("#login").hide(); |             $("#login").hide(); | ||||||
|  |             $("#search").show(); | ||||||
|             if (!query.columns) { |  | ||||||
|                 query.columns = 2; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             /** |             /** | ||||||
|              * Render authority views |              * Render authority views | ||||||
|              **/ |              **/ | ||||||
|             $("#view-dashboard").html(env.render('views/authority.html', { |             $("#view-dashboard").html(env.render('views/authority.html', { | ||||||
|                 session: session, |                 session: session, | ||||||
|                 window: window, |                 window: window | ||||||
|                 columns: query.columns, |             })); | ||||||
|                 column_width: 12 / query.columns, |  | ||||||
|                 authority_name: window.location.hostname })); |  | ||||||
|             $("time").timeago(); |             $("time").timeago(); | ||||||
|             if (session.authority) { |             if (session.authority) { | ||||||
|                 $("#log input").each(function(i, e) { |                 $("#log input").each(function(i, e) { | ||||||
| @@ -414,12 +585,8 @@ function loadAuthority(query) { | |||||||
|             $("#search").on("keyup", function() { |             $("#search").on("keyup", function() { | ||||||
|                 if (window.searchTimeout) { clearTimeout(window.searchTimeout); } |                 if (window.searchTimeout) { clearTimeout(window.searchTimeout); } | ||||||
|                 window.searchTimeout = setTimeout(function() { $(window).trigger("search"); }, 500); |                 window.searchTimeout = setTimeout(function() { $(window).trigger("search"); }, 500); | ||||||
|                 console.info("Setting timeout", window.searchTimeout); |  | ||||||
|  |  | ||||||
|             }); |             }); | ||||||
|  |  | ||||||
|             console.log("Features enabled:", session.features); |  | ||||||
|  |  | ||||||
|             if (session.request_submission_allowed) { |             if (session.request_submission_allowed) { | ||||||
|                 $("#request_submit").click(function() { |                 $("#request_submit").click(function() { | ||||||
|                     $(this).addClass("busy"); |                     $(this).addClass("busy"); | ||||||
| @@ -442,11 +609,9 @@ function loadAuthority(query) { | |||||||
|                             alert(e); |                             alert(e); | ||||||
|                         } |                         } | ||||||
|                     }); |                     }); | ||||||
|  |  | ||||||
|                 }); |                 }); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |  | ||||||
|             $("nav .nav-link.dashboard").removeClass("disabled").click(function() { |             $("nav .nav-link.dashboard").removeClass("disabled").click(function() { | ||||||
|                 $("#column-requests").show(); |                 $("#column-requests").show(); | ||||||
|                 $("#column-signed").show(); |                 $("#column-signed").show(); | ||||||
| @@ -458,31 +623,36 @@ function loadAuthority(query) { | |||||||
|              * Fetch log entries |              * Fetch log entries | ||||||
|              */ |              */ | ||||||
|             if (session.features.logging) { |             if (session.features.logging) { | ||||||
|                 if (query.columns == 4) { |                 if ($("#column-log:visible").length) { | ||||||
|                     loadLog(); |                     loadLog(); | ||||||
|                 } else { |                 } | ||||||
|                 $("nav .nav-link.log").removeClass("disabled").click(function() { |                 $("nav .nav-link.log").removeClass("disabled").click(function() { | ||||||
|  |                     loadLog(); | ||||||
|                     $("#column-requests").show(); |                     $("#column-requests").show(); | ||||||
|                     $("#column-signed").show(); |                     $("#column-signed").show(); | ||||||
|                     $("#column-revoked").show(); |                     $("#column-revoked").show(); | ||||||
|                     $("#column-log").hide(); |                     $("#column-log").hide(); | ||||||
|                 }); |                 }); | ||||||
|                 } |             } else { | ||||||
|  |                 console.info("Log disabled"); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
|  |  | ||||||
| function loadLog() { | function loadLog() { | ||||||
|     if (window.log_initialized) return; |     if (window.log_initialized) { | ||||||
|  |         console.info("Log already loaded"); | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |     console.info("Loading log..."); | ||||||
|     window.log_initialized = true; |     window.log_initialized = true; | ||||||
|     $.ajax({ |     $.ajax({ | ||||||
|         method: "GET", |         method: "GET", | ||||||
|         url: "/api/log/", |         url: "/api/log/?limit=100", | ||||||
|         dataType: "json", |         dataType: "json", | ||||||
|         success: function(entries, status, xhr) { |         success: function(entries, status, xhr) { | ||||||
|             console.info("Got", entries.length, "log entries"); |             console.info("Got", entries.length, "log entries"); | ||||||
|             console.info("j=", entries.length-1); |  | ||||||
|             for (var j = entries.length-1; j--; ) { |             for (var j = entries.length-1; j--; ) { | ||||||
|                 onLogEntry(entries[j]); |                 onLogEntry(entries[j]); | ||||||
|             }; |             }; | ||||||
|   | |||||||
							
								
								
									
										15
									
								
								certidude/static/snippets/ansible-site.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,15 @@ | |||||||
|  | - hosts:  {% for router in session.service.routers %} | ||||||
|  |   {{ router }}{% endfor %} | ||||||
|  |  | ||||||
|  |   roles: | ||||||
|  |     - role: certidude | ||||||
|  |       authority_name: {{ session.authority.hostname }} | ||||||
|  |  | ||||||
|  |     - role: ipsec_mesh | ||||||
|  |       mesh_name: mymesh | ||||||
|  |       authority_name: {{ session.authority.hostname }} | ||||||
|  |       ike: aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}! | ||||||
|  |       esp: aes128gcm16-aes128gmac-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}! | ||||||
|  |       auto: start | ||||||
|  |       nodes:{% for router in session.service.routers %} | ||||||
|  |         {{ router }}: 172.27.{{ loop.index }}.0/24{% endfor %} | ||||||
| @@ -1,25 +1,24 @@ | |||||||
| pip3 install git+https://github.com/laurivosandi/certidude/ | pip3 install git+https://github.com/laurivosandi/certidude/ | ||||||
| mkdir -p /etc/certidude/{client.conf.d,services.conf.d} | mkdir -p /etc/certidude/{client.conf.d,services.conf.d} | ||||||
| cat << EOF > /etc/certidude/client.conf.d/{{ authority_name }}.conf |  | ||||||
| [{{ authority_name }}] | cat << \EOF > /etc/certidude/client.conf.d/{{ session.authority.hostname }}.conf | ||||||
|  | [{{ session.authority.hostname }}] | ||||||
| trigger = interface up | trigger = interface up | ||||||
| common name = $HOSTNAME | common name = $HOSTNAME | ||||||
| system wide = true | system wide = true | ||||||
| EOF | EOF | ||||||
|  |  | ||||||
| cat << EOF > /etc/certidude/services.conf.d/{{ authority_name }}.conf | cat << EOF > /etc/certidude/services.conf.d/{{ session.authority.hostname }}.conf{% for router in session.service.routers %}{% if "ikev2" in session.service.protocols %} | ||||||
| {% for router in session.service.routers %}{% if "ikev2" in session.service.protocols %} |  | ||||||
| [IPSec to {{ router }}] | [IPSec to {{ router }}] | ||||||
| authority = {{ authority_name }} | authority = {{ session.authority.hostname }} | ||||||
| service = network-manager/strongswan | service = network-manager/strongswan | ||||||
| remote = {{ router }} | remote = {{ router }} | ||||||
| {% endif %}{% if "openvpn" in session.service.protocols %} | {% endif %}{% if "openvpn" in session.service.protocols %} | ||||||
| [OpenVPN to {{ router }}] | [OpenVPN to {{ router }}] | ||||||
| authority = {{ authority_name }} | authority = {{ session.authority.hostname }} | ||||||
| service = network-manager/openvpn | service = network-manager/openvpn | ||||||
| remote = {{ router }} | remote = {{ router }} | ||||||
| {% endif %}{% endfor %} | {% endif %}{% endfor %}EOF | ||||||
| EOF |  | ||||||
|  |  | ||||||
| certidude enroll | certidude enroll | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| # Create VPN gateway up/down script for reporting client IP addresses to CA | # Create VPN gateway up/down script for reporting client IP addresses to CA | ||||||
| cat <<\EOF > /etc/certidude/authority/{{ authority_name }}/updown | cat <<\EOF > /etc/certidude/authority/{{ session.authority.hostname }}/updown | ||||||
| #!/bin/sh | #!/bin/sh | ||||||
|  |  | ||||||
| CURL="curl -m 3 -f --key /etc/certidude/authority/{{ authority_name }}/host_key.pem --cert /etc/certidude/authority/{{ authority_name }}/host_cert.pem --cacert /etc/certidude/authority/{{ authority_name }}/ca_cert.pem https://{{ authority_name }}:8443/api/lease/" | CURL="curl -m 3 -f --key /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem --cert /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem --cacert /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem https://{{ session.authority.hostname }}:8443/api/lease/" | ||||||
|  |  | ||||||
| case $PLUTO_VERB in | case $PLUTO_VERB in | ||||||
|     up-client) $CURL --data-urlencode "outer_address=$PLUTO_PEER" --data-urlencode "inner_address=$PLUTO_PEER_SOURCEIP" --data-urlencode "client=$PLUTO_PEER_ID" ;; |     up-client) $CURL --data-urlencode "outer_address=$PLUTO_PEER" --data-urlencode "inner_address=$PLUTO_PEER_SOURCEIP" --data-urlencode "client=$PLUTO_PEER_ID" ;; | ||||||
| @@ -15,5 +15,5 @@ case $script_type in | |||||||
| esac | esac | ||||||
| EOF | EOF | ||||||
|  |  | ||||||
| chmod +x /etc/certidude/authority/{{ authority_name }}/updown | chmod +x /etc/certidude/authority/{{ session.authority.hostname }}/updown | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										97
									
								
								certidude/static/snippets/ios.mobileconfig
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,97 @@ | |||||||
|  | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||||||
|  | <plist version="1.0"> | ||||||
|  | <dict> | ||||||
|  |     <!-- https://developer.apple.com/library/content/featuredarticles/iPhoneConfigurationProfileRef/Introduction/Introduction.html --> | ||||||
|  |     <key>PayloadDisplayName</key> | ||||||
|  |     <string>{{ gateway }}</string> | ||||||
|  |     <!-- This is a reverse-DNS style unique identifier used to detect duplicate profiles --> | ||||||
|  |     <key>PayloadIdentifier</key> | ||||||
|  |     <string>org.example.vpn2</string> | ||||||
|  |     <key>PayloadUUID</key> | ||||||
|  |     <string>9f93912b-5fd2-4455-99fd-13b9a47b4581</string> | ||||||
|  |     <key>PayloadType</key> | ||||||
|  |     <string>Configuration</string> | ||||||
|  |     <key>PayloadVersion</key> | ||||||
|  |     <integer>1</integer> | ||||||
|  |     <key>PayloadContent</key> | ||||||
|  |     <array> | ||||||
|  |         <dict> | ||||||
|  |             <key>PayloadIdentifier</key> | ||||||
|  |             <string>org.example.vpn2.conf1</string> | ||||||
|  |             <key>PayloadUUID</key> | ||||||
|  |             <string>29e4456d-3f03-4f15-b46f-4225d89465b7</string> | ||||||
|  |             <key>PayloadType</key> | ||||||
|  |             <string>com.apple.vpn.managed</string> | ||||||
|  |             <key>PayloadVersion</key> | ||||||
|  |             <integer>1</integer> | ||||||
|  |             <key>UserDefinedName</key> | ||||||
|  |             <string>{{ gateway }}</string> | ||||||
|  |             <key>VPNType</key> | ||||||
|  |             <string>IKEv2</string> | ||||||
|  |             <key>IKEv2</key> | ||||||
|  |             <dict> | ||||||
|  |                 <key>RemoteAddress</key> | ||||||
|  |                 <string>{{ gateway }}</string> | ||||||
|  |                 <key>RemoteIdentifier</key> | ||||||
|  |                 <string>{{ gateway }}</string> | ||||||
|  |                 <key>LocalIdentifier</key> | ||||||
|  |                 <string>{{ common_name }}</string> | ||||||
|  |                 <key>ServerCertificateIssuerCommonName</key> | ||||||
|  |                 <string>{{ session.authority.certificate.common_name }}</string> | ||||||
|  |                 <key>ServerCertificateCommonName</key> | ||||||
|  |                 <string>{{ gateway }}</string> | ||||||
|  |                 <key>AuthenticationMethod</key> | ||||||
|  |                 <string>Certificate</string> | ||||||
|  |                 <key>IKESecurityAssociationParameters</key> | ||||||
|  |                 <dict> | ||||||
|  |                     <key>EncryptionAlgorithm</key> | ||||||
|  |                     <string>AES-256</string> | ||||||
|  |                     <key>IntegrityAlgorithm</key> | ||||||
|  |                     <string>SHA2-384</string> | ||||||
|  |                     <key>DiffieHellmanGroup</key> | ||||||
|  |                     <integer>14</integer> | ||||||
|  |                 </dict> | ||||||
|  |                 <key>ChildSecurityAssociationParameters</key> | ||||||
|  |                 <dict> | ||||||
|  |                     <key>EncryptionAlgorithm</key> | ||||||
|  |                     <string>AES-128-GCM</string> | ||||||
|  |                     <key>IntegrityAlgorithm</key> | ||||||
|  |                     <string>SHA2-256</string> | ||||||
|  |                     <key>DiffieHellmanGroup</key> | ||||||
|  |                     <integer>14</integer> | ||||||
|  |                 </dict> | ||||||
|  |                 <key>EnablePFS</key> | ||||||
|  |                 <integer>1</integer> | ||||||
|  |                 <key>PayloadCertificateUUID</key> | ||||||
|  |                 <string>d60488c6-328e-4944-9c8d-61db8095c865</string> | ||||||
|  |             </dict> | ||||||
|  |         </dict> | ||||||
|  |         <dict> | ||||||
|  |             <key>PayloadIdentifier</key> | ||||||
|  |             <string>ee.k-space.ca2.client</string> | ||||||
|  |             <key>PayloadUUID</key> | ||||||
|  |             <string>d60488c6-328e-4944-9c8d-61db8095c865</string> | ||||||
|  |             <key>PayloadType</key> | ||||||
|  |             <string>com.apple.security.pkcs12</string> | ||||||
|  |             <key>PayloadVersion</key> | ||||||
|  |             <integer>1</integer> | ||||||
|  |             <key>PayloadContent</key> | ||||||
|  |             <data>{{ p12 }}</data> | ||||||
|  |         </dict> | ||||||
|  |         <dict> | ||||||
|  |             <key>PayloadIdentifier</key> | ||||||
|  |             <string>org.example.ca</string> | ||||||
|  |             <key>PayloadUUID</key> | ||||||
|  |             <string>64988b2c-33e0-4adf-a432-6fbcae543408</string> | ||||||
|  |             <key>PayloadType</key> | ||||||
|  |             <string>com.apple.security.root</string> | ||||||
|  |             <key>PayloadVersion</key> | ||||||
|  |             <integer>1</integer> | ||||||
|  |             <!-- This is the Base64 (PEM) encoded CA certificate --> | ||||||
|  |             <key>PayloadContent</key> | ||||||
|  |             <data>{{ ca }}</data> | ||||||
|  |         </dict> | ||||||
|  |     </array> | ||||||
|  | </dict> | ||||||
|  | </plist> | ||||||
|  |  | ||||||
							
								
								
									
										35
									
								
								certidude/static/snippets/openvpn-client.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,35 @@ | |||||||
|  | client | ||||||
|  | nobind{% for router in session.service.routers %} | ||||||
|  | remote {{ router }}{% endfor %} | ||||||
|  | proto tcp-client | ||||||
|  | port 443 | ||||||
|  | tls-version-min 1.2 | ||||||
|  | tls-cipher TLS-{% if session.authority.certificate.algorithm == "ec" %}ECDHE-ECDSA{% else %}DHE-RSA{% endif %}-WITH-AES-256-GCM-SHA384 | ||||||
|  | cipher AES-128-GCM | ||||||
|  | auth SHA384 | ||||||
|  | mute-replay-warnings | ||||||
|  | reneg-sec 0 | ||||||
|  | remote-cert-tls server | ||||||
|  | dev tun | ||||||
|  | persist-tun | ||||||
|  | persist-key | ||||||
|  | {% if ca %} | ||||||
|  | <ca> | ||||||
|  | {{ ca }} | ||||||
|  | </ca> | ||||||
|  | {% else %}ca /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem{% endif %} | ||||||
|  | {% if key %} | ||||||
|  | <key> | ||||||
|  | {{ key }} | ||||||
|  | </key> | ||||||
|  | {% else %}key /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem{% endif %} | ||||||
|  | {% if cert %} | ||||||
|  | <cert> | ||||||
|  | {{ cert }} | ||||||
|  | </cert> | ||||||
|  | {% else %}cert /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem{% endif %} | ||||||
|  |  | ||||||
|  | # To enable dynamic DNS server update on Ubuntu, uncomment these | ||||||
|  | #script-security 2 | ||||||
|  | #up /etc/openvpn/update-resolv-conf | ||||||
|  | #down /etc/openvpn/update-resolv-conf | ||||||
| @@ -2,26 +2,19 @@ | |||||||
| which apt && apt install openvpn | which apt && apt install openvpn | ||||||
| which dnf && dnf install openvpn | which dnf && dnf install openvpn | ||||||
|  |  | ||||||
| cat > /etc/openvpn/{{ authority_name }}.conf << EOF | # Create OpenVPN configuration file | ||||||
| client | cat > /etc/openvpn/{{ session.authority.hostname }}.conf << EOF | ||||||
| nobind | {% include "snippets/openvpn-client.conf" %} | ||||||
| {% for router in session.service.routers %} |  | ||||||
| remote {{ router }} 1194 udp |  | ||||||
| remote {{ router }} 443 tcp-client |  | ||||||
| {% endfor %} |  | ||||||
| tls-version-min 1.2 |  | ||||||
| tls-cipher TLS-{% if session.authority.certificate.algorithm == "ec" %}ECDHE-ECDSA{% else %}DHE-RSA{% endif %}-WITH-AES-128-GCM-SHA384 |  | ||||||
| cipher AES-128-GCM |  | ||||||
| auth SHA384 |  | ||||||
| mute-replay-warnings |  | ||||||
| reneg-sec 0 |  | ||||||
| remote-cert-tls server |  | ||||||
| dev tun |  | ||||||
| persist-tun |  | ||||||
| persist-key |  | ||||||
| ca /etc/certidude/authority/{{ authority_name }}/ca_cert.pem |  | ||||||
| key /etc/certidude/authority/{{ authority_name }}/host_key.pem |  | ||||||
| cert /etc/certidude/authority/{{ authority_name }}/host_cert.pem |  | ||||||
| EOF | EOF | ||||||
|  |  | ||||||
|  | # Restart OpenVPN service | ||||||
| systemctl restart openvpn | systemctl restart openvpn | ||||||
|  | {# | ||||||
|  |  | ||||||
|  | Some notes: | ||||||
|  |  | ||||||
|  | - Ubuntu 16.04 ships OpenVPN 2.3 which doesn't support AES-128-GCM | ||||||
|  | - NetworkManager's OpenVPN profile importer doesn't understand multiple remotes | ||||||
|  | - Tunnelblick and OpenVPN Connect apps don't have a method to update CRL | ||||||
|  |  | ||||||
|  | #} | ||||||
|   | |||||||
| @@ -65,9 +65,9 @@ for section in s2c_tcp s2c_udp; do | |||||||
| # Common paths | # Common paths | ||||||
| uci set openvpn.$section.script_security=2 | uci set openvpn.$section.script_security=2 | ||||||
| uci set openvpn.$section.client_connect='/etc/certidude/updown' | uci set openvpn.$section.client_connect='/etc/certidude/updown' | ||||||
| uci set openvpn.$section.key='/etc/certidude/authority/{{ authority_name }}/host_key.pem' | uci set openvpn.$section.key='/etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem' | ||||||
| uci set openvpn.$section.cert='/etc/certidude/authority/{{ authority_name }}/host_cert.pem' | uci set openvpn.$section.cert='/etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem' | ||||||
| uci set openvpn.$section.ca='/etc/certidude/authority/{{ authority_name }}/ca_cert.pem' | uci set openvpn.$section.ca='/etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem' | ||||||
| {% if session.authority.certificate.algorithm != "ec" %}uci set openvpn.$section.dh='/etc/certidude/dh.pem'{% endif %} | {% if session.authority.certificate.algorithm != "ec" %}uci set openvpn.$section.dh='/etc/certidude/dh.pem'{% endif %} | ||||||
| uci set openvpn.$section.enabled=1 | uci set openvpn.$section.enabled=1 | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| curl -f -L -H "Content-type: application/pkcs10" \ | curl --cert-status -f -L -H "Content-type: application/pkcs10" \ | ||||||
|     --cacert /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \ |     --cacert /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem \ | ||||||
|     --key /etc/certidude/authority/{{ authority_name }}/host_key.pem \ |     --key /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem \ | ||||||
|     --cert /etc/certidude/authority/{{ authority_name }}/host_cert.pem \ |     --cert /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem \ | ||||||
|     --data-binary @/etc/certidude/authority/{{ authority_name }}/host_req.pem \ |     --data-binary @/etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem \ | ||||||
|     -o /etc/certidude/authority/{{ authority_name }}/host_cert.pem \ |     -o /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem \ | ||||||
|     'https://{{ authority_name }}:8443/api/request/?wait=yes' |     'https://{{ session.authority.hostname }}:8443/api/request/?wait=yes' | ||||||
|   | |||||||
| @@ -1,15 +1,11 @@ | |||||||
|  | # Use short hostname as common name | ||||||
| test -e /sbin/uci && NAME=$(uci get system.@system[0].hostname) | test -e /sbin/uci && NAME=$(uci get system.@system[0].hostname) | ||||||
| test -e /bin/hostname && NAME=$(hostname) | test -e /bin/hostname && NAME=$(hostname) | ||||||
| test -n "$NAME" || NAME=$(cat /proc/sys/kernel/hostname) | test -n "$NAME" || NAME=$(cat /proc/sys/kernel/hostname) | ||||||
|  |  | ||||||
| {% include "snippets/update-trust.sh" %} |  | ||||||
|  |  | ||||||
| {% include "snippets/request-common.sh" %} | {% include "snippets/request-common.sh" %} | ||||||
|  | # Submit CSR and save signed certificate | ||||||
| curl -f -L -H "Content-type: application/pkcs10" \ | curl --cert-status -f -L -H "Content-type: application/pkcs10" \ | ||||||
| --data-binary @/etc/certidude/authority/{{ authority_name }}/host_req.pem \ |   --data-binary @/etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem \ | ||||||
| -o /etc/certidude/authority/{{ authority_name }}/host_cert.pem \ |   -o /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem \ | ||||||
| 'http://{{ authority_name }}/api/request/?wait=yes&autosign=yes' |   'http://{{ session.authority.hostname }}/api/request/?wait=yes&autosign=yes' | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,14 +1,16 @@ | |||||||
| echo {{ session.authority.certificate.md5sum }} /etc/certidude/authority/{{ authority_name }}/ca_cert.pem | md5sum -c \ | # Delete CA certificate if checksum doesn't match | ||||||
|  || rm -fv /etc/certidude/authority/{{ authority_name }}/*.pem | echo {{ session.authority.certificate.md5sum }} /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem | md5sum -c \ | ||||||
|  |  || rm -fv /etc/certidude/authority/{{ session.authority.hostname }}/*.pem | ||||||
| {% include "snippets/store-authority.sh" %} | {% include "snippets/store-authority.sh" %} | ||||||
| test -e /etc/certidude/authority/{{ authority_name }}/host_key.pem \ | {% include "snippets/update-trust.sh" %} | ||||||
|  | # Generate private key | ||||||
|  | test -e /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem \ | ||||||
|  || {% if session.authority.certificate.algorithm == "ec" %}openssl ecparam -name secp384r1 -genkey -noout \ |  || {% if session.authority.certificate.algorithm == "ec" %}openssl ecparam -name secp384r1 -genkey -noout \ | ||||||
|  -out /etc/certidude/authority/{{ authority_name }}/host_key.pem{% else %}openssl genrsa \ |  -out /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem{% else %}openssl genrsa \ | ||||||
|  -out /etc/certidude/authority/{{ authority_name }}/host_key.pem 2048{% endif %} |  -out /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem 2048{% endif %} | ||||||
| test -e /etc/certidude/authority/{{ authority_name }}/host_req.pem \ | test -e /etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem \ | ||||||
|  || openssl req -new -sha384 -subj "/CN=$NAME" \ |  || openssl req -new -sha384 -subj "/CN=$NAME" \ | ||||||
|  -key /etc/certidude/authority/{{ authority_name }}/host_key.pem \ |  -key /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem \ | ||||||
|  -out /etc/certidude/authority/{{ authority_name }}/host_req.pem |  -out /etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem | ||||||
| echo "If CSR submission fails, you can copy paste it to Certidude:" | echo "If CSR submission fails, you can copy paste it to Certidude:" | ||||||
| cat /etc/certidude/authority/{{ authority_name }}/host_req.pem | cat /etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,13 +1,12 @@ | |||||||
|  | # Use fully qualified name | ||||||
| test -e /sbin/uci && NAME=$(nslookup $(uci get network.wan.ipaddr) |  grep "name =" | head -n1 | cut -d "=" -f 2 | xargs) | test -e /sbin/uci && NAME=$(nslookup $(uci get network.wan.ipaddr) |  grep "name =" | head -n1 | cut -d "=" -f 2 | xargs) | ||||||
| test -e /bin/hostname && NAME=$(hostname -f) | test -e /bin/hostname && NAME=$(hostname -f) | ||||||
| test -n "$NAME" || NAME=$(cat /proc/sys/kernel/hostname) | test -n "$NAME" || NAME=$(cat /proc/sys/kernel/hostname) | ||||||
|  |  | ||||||
| {% include "snippets/update-trust.sh" %} |  | ||||||
|  |  | ||||||
| {% include "snippets/request-common.sh" %} | {% include "snippets/request-common.sh" %} | ||||||
|  | # Submit CSR and save signed certificate | ||||||
| curl -f -L -H "Content-type: application/pkcs10" \ | curl --cert-status -f -L -H "Content-type: application/pkcs10" \ | ||||||
|     --cacert /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \ |     --cacert /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem \ | ||||||
|     --data-binary @/etc/certidude/authority/{{ authority_name }}/host_req.pem \ |     --data-binary @/etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem \ | ||||||
|     -o /etc/certidude/authority/{{ authority_name }}/host_cert.pem \ |     -o /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem \ | ||||||
|     'https://{{ authority_name }}:8443/api/request/?wait=yes' |     'https://{{ session.authority.hostname }}:8443/api/request/?wait=yes' | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| mkdir -p /etc/certidude/authority/{{ authority_name }}/ | # Save CA certificate | ||||||
| test -e /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \ | mkdir -p /etc/certidude/authority/{{ session.authority.hostname }}/ | ||||||
|  || cat << EOF > /etc/certidude/authority/{{ authority_name }}/ca_cert.pem | test -e /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem \ | ||||||
|  |  || cat << EOF > /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem | ||||||
| {{ session.authority.certificate.blob }}EOF | {{ session.authority.certificate.blob }}EOF | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,10 +1,10 @@ | |||||||
| cat > /etc/ipsec.conf << EOF | cat > /etc/ipsec.conf << EOF | ||||||
|  | config setup | ||||||
|  |     strictcrlpolicy=yes | ||||||
|  |  | ||||||
| ca {{ authority_name }} | ca {{ session.authority.hostname }} | ||||||
|     auto=add |     auto=add | ||||||
|     cacert=/etc/certidude/authority/{{ authority_name }}/ca_cert.pem |     cacert=/etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem | ||||||
| {% if session.features.crl %}    crluri=http://{{ authority_name }}/api/revoked/{% endif %} |  | ||||||
| {% if session.features.ocsp %}    ocspuri=http://{{ authority_name }}/api/ocsp/{% endif %} |  | ||||||
|  |  | ||||||
| conn client-to-site | conn client-to-site | ||||||
|     auto=start |     auto=start | ||||||
| @@ -12,7 +12,7 @@ conn client-to-site | |||||||
|     rightsubnet=0.0.0.0/0 |     rightsubnet=0.0.0.0/0 | ||||||
|     rightca="{{ session.authority.certificate.distinguished_name }}" |     rightca="{{ session.authority.certificate.distinguished_name }}" | ||||||
|     left=%defaultroute |     left=%defaultroute | ||||||
|     leftcert=/etc/certidude/authority/{{ authority_name }}/host_cert.pem |     leftcert=/etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem | ||||||
|     leftsourceip=%config |     leftsourceip=%config | ||||||
|     leftca="{{ session.authority.certificate.distinguished_name }}" |     leftca="{{ session.authority.certificate.distinguished_name }}" | ||||||
|     keyexchange=ikev2 |     keyexchange=ikev2 | ||||||
| @@ -21,9 +21,8 @@ conn client-to-site | |||||||
|     closeaction=restart |     closeaction=restart | ||||||
|     ike=aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}! |     ike=aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}! | ||||||
|     esp=aes128gcm16-aes128gmac-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}! |     esp=aes128gcm16-aes128gmac-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}! | ||||||
|  |  | ||||||
| EOF | EOF | ||||||
|  |  | ||||||
| echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} {{ authority_name }}.pem" > /etc/ipsec.secrets | echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} {{ session.authority.hostname }}.pem" > /etc/ipsec.secrets | ||||||
|  |  | ||||||
| ipsec restart | ipsec restart apparmor | ||||||
|   | |||||||
| @@ -6,11 +6,12 @@ test -e /etc/strongswan && test -e /etc/ipsec.d || ln -s strongswan/ipsec.d /etc | |||||||
| test -e /etc/strongswan && test -e /etc/ipsec.secrets || ln -s strongswan/ipsec.secrets /etc/ipsec.secrets | test -e /etc/strongswan && test -e /etc/ipsec.secrets || ln -s strongswan/ipsec.secrets /etc/ipsec.secrets | ||||||
|  |  | ||||||
| # Set SELinux context | # Set SELinux context | ||||||
| chcon --type=home_cert_t /etc/certidude/authority/{{ authority_name }}/ca_cert.pem /etc/ipsec.d/cacerts/{{ authority_name }}.pem | chcon --type=home_cert_t /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem /etc/ipsec.d/cacerts/{{ session.authority.hostname }}.pem | ||||||
| chcon --type=home_cert_t  /etc/certidude/authority/{{ authority_name }}/host_cert.pem /etc/ipsec.d/certs/{{ authority_name }}.pem | chcon --type=home_cert_t  /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem /etc/ipsec.d/certs/{{ session.authority.hostname }}.pem | ||||||
| chcon --type=home_cert_t  /etc/certidude/authority/{{ authority_name }}/host_key.pem /etc/ipsec.d/private/{{ authority_name }}.pem | chcon --type=home_cert_t  /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem /etc/ipsec.d/private/{{ session.authority.hostname }}.pem | ||||||
|  |  | ||||||
| # Patch AppArmor | # Patch AppArmor | ||||||
| cat << EOF > /etc/apparmor.d/local/usr.lib.ipsec.charon | cat << EOF > /etc/apparmor.d/local/usr.lib.ipsec.charon | ||||||
| /etc/certidude/authority/** | /etc/certidude/authority/** r, | ||||||
| EOF | EOF | ||||||
|  | systemctl restart | ||||||
|   | |||||||
| @@ -4,19 +4,17 @@ config setup | |||||||
|     strictcrlpolicy=yes |     strictcrlpolicy=yes | ||||||
|     uniqueids=yes |     uniqueids=yes | ||||||
|  |  | ||||||
| ca {{ authority_name }} | ca {{ session.authority.hostname }} | ||||||
|     auto=add |     auto=add | ||||||
|     cacert=/etc/certidude/authority/{{ authority_name }}/ca_cert.pem |     cacert=/etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem | ||||||
| {% if session.features.crl %}    crluri=http://{{ authority_name }}/api/revoked/{% endif %} |  | ||||||
| {% if session.features.ocsp %}    ocspuri=http://{{ authority_name }}/api/ocsp/{% endif %} |  | ||||||
|  |  | ||||||
| conn default-{{ authority_name }} | conn default-{{ session.authority.hostname }} | ||||||
|     ike=aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}! |     ike=aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}! | ||||||
|     esp=aes128gcm16-aes128gmac-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}! |     esp=aes128gcm16-aes128gmac-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}! | ||||||
|     left=$(uci get network.wan.ipaddr) # Bind to this IP address |     left=$(uci get network.wan.ipaddr) # Bind to this IP address | ||||||
|     leftid={{ session.service.routers | first }} |     leftid={{ session.service.routers | first }} | ||||||
|     leftupdown=/etc/certidude/authority/{{ authority_name }}/updown |     leftupdown=/etc/certidude/authority/{{ session.authority.hostname }}/updown | ||||||
|     leftcert=/etc/certidude/authority/{{ authority_name }}/host_cert.pem |     leftcert=/etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem | ||||||
|     leftsubnet=$(uci get network.lan.ipaddr | cut -d . -f 1-3).0/24 # Subnets pushed to roadwarriors |     leftsubnet=$(uci get network.lan.ipaddr | cut -d . -f 1-3).0/24 # Subnets pushed to roadwarriors | ||||||
|     leftdns=$(uci get network.lan.ipaddr) # IP of DNS server advertised to roadwarriors |     leftdns=$(uci get network.lan.ipaddr) # IP of DNS server advertised to roadwarriors | ||||||
|     leftca="{{ session.authority.certificate.distinguished_name }}" |     leftca="{{ session.authority.certificate.distinguished_name }}" | ||||||
| @@ -27,15 +25,15 @@ conn default-{{ authority_name }} | |||||||
|  |  | ||||||
| conn site-to-clients | conn site-to-clients | ||||||
|     auto=add |     auto=add | ||||||
|     also=default-{{ authority_name }} |     also=default-{{ session.authority.hostname }} | ||||||
|  |  | ||||||
| conn site-to-client1 | conn site-to-client1 | ||||||
|     auto=ignore |     auto=ignore | ||||||
|     also=default-{{ authority_name }} |     also=default-{{ session.authority.hostname }} | ||||||
|     rightid="CN=*, OU=IP Camera, O=*, DC=*, DC=*, DC=*" |     rightid="CN=*, OU=IP Camera, O=*, DC=*, DC=*, DC=*" | ||||||
|     rightsourceip=172.21.0.1 |     rightsourceip=172.21.0.1 | ||||||
|  |  | ||||||
| EOF | EOF | ||||||
|  |  | ||||||
| echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} /etc/certidude/authority/{{ authority_name }}/host_key.pem" > /etc/ipsec.secrets | echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem" > /etc/ipsec.secrets | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,7 +1,9 @@ | |||||||
|  | # Insert into Fedora trust store. Applies to curl, Firefox, Chrome, Chromium | ||||||
| test -e /etc/pki/ca-trust/source/anchors \ | test -e /etc/pki/ca-trust/source/anchors \ | ||||||
|  && ln -s /etc/certidude/authority/{{ authority_name }}/ca_cert.pem /etc/pki/ca-trust/source/anchors/{{ authority_name }} \ |  && ln -s /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem /etc/pki/ca-trust/source/anchors/{{ session.authority.hostname }} \ | ||||||
|  && update-ca-trust |  && update-ca-trust | ||||||
| test -e /usr/local/share/ca-certificates/ \ |  | ||||||
|  && ln -s /etc/certidude/authority/{{ authority_name }}/ca_cert.pem /usr/local/share/ca-certificates/{{ authority_name }}.crt \ |  | ||||||
|  && update-ca-certificates |  | ||||||
|  |  | ||||||
|  | # Insert into Ubuntu trust store, only applies to curl | ||||||
|  | test -e /usr/local/share/ca-certificates/ \ | ||||||
|  |  && ln -s /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem /usr/local/share/ca-certificates/{{ session.authority.hostname }}.crt \ | ||||||
|  |  && update-ca-certificates | ||||||
|   | |||||||
| @@ -1,7 +1,6 @@ | |||||||
| # Install CA certificate | # Install CA certificate | ||||||
| @" | @" | ||||||
| {{ session.authority.certificate.blob }} | {{ session.authority.certificate.blob }}"@ | Out-File ca_cert.pem | ||||||
| "@ | Out-File ca_cert.pem |  | ||||||
| {% if session.authority.certificate.algorithm == "ec" %} | {% if session.authority.certificate.algorithm == "ec" %} | ||||||
| Import-Certificate -FilePath ca_cert.pem -CertStoreLocation Cert:\LocalMachine\Root | Import-Certificate -FilePath ca_cert.pem -CertStoreLocation Cert:\LocalMachine\Root | ||||||
| {% else %} | {% else %} | ||||||
| @@ -25,25 +24,25 @@ KeyAlgorithm = ECDSA_P384 | |||||||
| KeyLength = 2048 | KeyLength = 2048 | ||||||
| {% endif %}"@ | Out-File req.inf | {% endif %}"@ | Out-File req.inf | ||||||
| C:\Windows\system32\certreq.exe -new -f -q req.inf host_csr.pem | C:\Windows\system32\certreq.exe -new -f -q req.inf host_csr.pem | ||||||
| Invoke-WebRequest -TimeoutSec 900 -Uri 'https://{{ authority_name }}:8443/api/request/?wait=yes&autosign=yes' -InFile host_csr.pem -ContentType application/pkcs10 -Method POST  -MaximumRedirection 3 -OutFile host_cert.pem | Invoke-WebRequest -TimeoutSec 900 -Uri 'https://{{ session.authority.hostname }}:8443/api/request/?wait=yes&autosign=yes' -InFile host_csr.pem -ContentType application/pkcs10 -Method POST  -MaximumRedirection 3 -OutFile host_cert.pem | ||||||
|  |  | ||||||
| # Import certificate | # Import certificate | ||||||
| {% if session.authority.certificate.algorithm == "ec" %}Import-Certificate -FilePath host_cert.pem -CertStoreLocation Cert:\LocalMachine\My | {% if session.authority.certificate.algorithm == "ec" %}Import-Certificate -FilePath host_cert.pem -CertStoreLocation Cert:\LocalMachine\My | ||||||
| {% else %}C:\Windows\system32\certutil.exe -addstore My host_cert.pem | {% else %}C:\Windows\system32\certutil.exe -addstore My host_cert.pem | ||||||
| {% endif %} | {% endif %} | ||||||
| # Set up IPSec VPN tunnel |  | ||||||
| Remove-VpnConnection -AllUserConnection -Force k-space | {% for router in session.service.routers %} | ||||||
|  | # Set up IPSec VPN tunnel to {{ router }} | ||||||
|  | Remove-VpnConnection -AllUserConnection -Force "IPSec to {{ router }}" | ||||||
| Add-VpnConnection ` | Add-VpnConnection ` | ||||||
|     -Name k-space ` |     -Name "IPSec to {{ router }}" ` | ||||||
|     -ServerAddress guests.k-space.ee ` |     -ServerAddress {{ router }} ` | ||||||
|     -AuthenticationMethod MachineCertificate ` |     -AuthenticationMethod MachineCertificate ` | ||||||
|     -SplitTunneling ` |     -SplitTunneling ` | ||||||
|     -TunnelType ikev2 ` |     -TunnelType ikev2 ` | ||||||
|     -PassThru -AllUserConnection |     -PassThru -AllUserConnection | ||||||
|  |  | ||||||
| # Security hardening |  | ||||||
| Set-VpnConnectionIPsecConfiguration ` | Set-VpnConnectionIPsecConfiguration ` | ||||||
|     -ConnectionName k-space ` |     -ConnectionName "IPSec to {{ router }}" ` | ||||||
|     -AuthenticationTransformConstants GCMAES128 ` |     -AuthenticationTransformConstants GCMAES128 ` | ||||||
|     -CipherTransformConstants GCMAES128 ` |     -CipherTransformConstants GCMAES128 ` | ||||||
|     -EncryptionMethod AES256 ` |     -EncryptionMethod AES256 ` | ||||||
| @@ -51,6 +50,8 @@ Set-VpnConnectionIPsecConfiguration ` | |||||||
|     -DHGroup {% if session.authority.certificate.algorithm == "ec" %}ECP384{% else %}Group14{% endif %} ` |     -DHGroup {% if session.authority.certificate.algorithm == "ec" %}ECP384{% else %}Group14{% endif %} ` | ||||||
|     -PfsGroup {% if session.authority.certificate.algorithm == "ec" %}ECP384{% else %}PFS2048{% endif %} ` |     -PfsGroup {% if session.authority.certificate.algorithm == "ec" %}ECP384{% else %}PFS2048{% endif %} ` | ||||||
|     -PassThru -AllUserConnection -Force |     -PassThru -AllUserConnection -Force | ||||||
|  | {% endfor %} | ||||||
|  |  | ||||||
| {# | {# | ||||||
| AuthenticationTransformConstants - ESP integrity algorithm, one of: None MD596 SHA196 SHA256128 GCMAES128 GCMAES192 GCMAES256 | AuthenticationTransformConstants - ESP integrity algorithm, one of: None MD596 SHA196 SHA256128 GCMAES128 GCMAES192 GCMAES256 | ||||||
| CipherTransformConstants - ESP symmetric cipher, one of: DES DES3 AES128 AES192 AES256 GCMAES128 GCMAES192 GCMAES256 | CipherTransformConstants - ESP symmetric cipher, one of: DES DES3 AES128 AES192 AES256 GCMAES128 GCMAES192 GCMAES256 | ||||||
|   | |||||||
| @@ -5,21 +5,71 @@ | |||||||
|         <button type="button" class="close" data-dismiss="modal">×</button> |         <button type="button" class="close" data-dismiss="modal">×</button> | ||||||
|         <h4 class="modal-title">Request submission</h4> |         <h4 class="modal-title">Request submission</h4> | ||||||
|       </div> |       </div> | ||||||
|       <form action="/api/request/" method="post"> |  | ||||||
|       <div class="modal-body"> |       <div class="modal-body"> | ||||||
|           <h5>Certidude client</h5> |         <ul class="nav nav-pills" id="myTab" role="tablist"> | ||||||
|  |           <li class="nav-item"> | ||||||
|  |             <a class="nav-link active" id="home-tab" data-toggle="tab" href="#snippet-certidude" role="tab" aria-controls="certidude" aria-selected="true">Certidude</a> | ||||||
|  |           </li> | ||||||
|  |  | ||||||
|  |           <li class="nav-item"> | ||||||
|  |             <a class="nav-link" id="profile-tab" data-toggle="tab" href="#snippet-windows" role="tab" aria-controls="windows" aria-selected="false">Windows</a> | ||||||
|  |           </li> | ||||||
|  |  | ||||||
|  |           <li class="nav-item"> | ||||||
|  |             <a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-unix" role="tab" aria-controls="unix" aria-selected="false">UNIX</a> | ||||||
|  |           </li> | ||||||
|  |  | ||||||
|  |           {% if "openvpn" in session.service.protocols %} | ||||||
|  |             <li class="nav-item"> | ||||||
|  |               <a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-openvpn" role="tab" aria-controls="openvpn" aria-selected="false">OpenVPN</a> | ||||||
|  |             </li> | ||||||
|  |           {% endif %} | ||||||
|  |  | ||||||
|  |           {% if "ikev2" in session.service.protocols %} | ||||||
|  |             <li class="nav-item"> | ||||||
|  |               <a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-strongswan" role="tab" aria-controls="strongswan" aria-selected="false">StrongSwan</a> | ||||||
|  |             </li> | ||||||
|  |           {% endif %} | ||||||
|  |  | ||||||
|  |           <li class="nav-item"> | ||||||
|  |             <a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-ansible" role="tab" aria-controls="ansible" aria-selected="false">Ansible</a> | ||||||
|  |           </li> | ||||||
|  |  | ||||||
|  |           <li class="nav-item"> | ||||||
|  |             <a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-lede" role="tab" aria-controls="lede" aria-selected="false">LEDE</a> | ||||||
|  |           </li> | ||||||
|  |  | ||||||
|  |           {% if session.authorization.scep_subnets %} | ||||||
|  |           <li class="nav-item"> | ||||||
|  |             <a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-scep" role="tab" aria-controls="scep" aria-selected="false">SCEP</a> | ||||||
|  |           </li> | ||||||
|  |           {% endif %} | ||||||
|  |  | ||||||
|  |           <li class="nav-item"> | ||||||
|  |             <a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-copypaste" role="tab" aria-controls="copypaste" aria-selected="false">Copypasta</a> | ||||||
|  |           </li> | ||||||
|  |  | ||||||
|  |         </ul> | ||||||
|  |         <div class="tab-content" id="myTabContent"> | ||||||
|  |           <!-- Certidude client --> | ||||||
|  |           <div class="tab-pane fade show active" id="snippet-certidude" role="tabpanel" aria-labelledby="certidude"> | ||||||
|             <p>On Ubuntu or Fedora:</p> |             <p>On Ubuntu or Fedora:</p> | ||||||
|             <div class="highlight"> |             <div class="highlight"> | ||||||
|               <pre class="code"><code>{% include "snippets/certidude-client.sh" %}</code></pre> |               <pre class="code"><code>{% include "snippets/certidude-client.sh" %}</code></pre> | ||||||
|             </div> |             </div> | ||||||
|  |           </div> | ||||||
|  |  | ||||||
|  |           <!-- Windows --> | ||||||
|  |           <div class="tab-pane fade" id="snippet-windows" role="tabpanel" aria-labelledby="windows"> | ||||||
|  |             <p>On Windows {% if session.authority.certificate.algorithm == "ec" %}10{% else %}7 and up{% endif %} execute following PowerShell script</p> | ||||||
|             {% if "ikev2" in session.service.protocols %} |             {% if "ikev2" in session.service.protocols %} | ||||||
|             <h5>Windows {% if session.authority.certificate.algorithm == "ec" %}10{% else %}7 and up{% endif %}</h5> |  | ||||||
|             <p>On Windows execute following PowerShell script</p> |  | ||||||
|               <div class="highlight"><pre class="code"><code>{% include "snippets/windows.ps1" %}</code></pre></div> |               <div class="highlight"><pre class="code"><code>{% include "snippets/windows.ps1" %}</code></pre></div> | ||||||
|             {% endif %} |             {% endif %} | ||||||
|  |           </div> | ||||||
|  |  | ||||||
|  |           <!-- UNIX-like --> | ||||||
|  |           <div class="tab-pane fade" id="snippet-unix" role="tabpanel" aria-labelledby="unix"> | ||||||
|  |  | ||||||
|           <h5>UNIX & UNIX-like</h5> |  | ||||||
|             <p>For client certificates generate key pair and submit the signing request with common name set to short hostname:</p> |             <p>For client certificates generate key pair and submit the signing request with common name set to short hostname:</p> | ||||||
|             <div class="highlight"> |             <div class="highlight"> | ||||||
|  |  | ||||||
| @@ -32,24 +82,20 @@ | |||||||
|             </div> |             </div> | ||||||
|  |  | ||||||
|             <p>To renew:</p> |             <p>To renew:</p> | ||||||
|  |  | ||||||
|             <div class="highlight"> |             <div class="highlight"> | ||||||
|             <pre class="code"><code>{% include "snippets/renew.sh" %}</code></pre> |             <pre class="code"><code>{% include "snippets/renew.sh" %}</code></pre> | ||||||
|             </div> |             </div> | ||||||
|  |           </div> | ||||||
|  |  | ||||||
|           {% if "openvpn" in session.service.protocols %} |           <!-- OpenVPN as client --> | ||||||
|             <h5>OpenVPN as client</h5> |           <div class="tab-pane fade" id="snippet-openvpn" role="tabpanel" aria-labelledby="openvpn"> | ||||||
|  |  | ||||||
|             <p>First acquire certificates using the snippet above.</p> |             <p>First acquire certificates using the snippet above.</p> | ||||||
|  |  | ||||||
|             <p>Then install software:</p> |             <p>Then install software:</p> | ||||||
|  |  | ||||||
|             <div class="highlight"><pre class="code"><code>{% include "snippets/openvpn-client.sh" %}</code></pre></div> |             <div class="highlight"><pre class="code"><code>{% include "snippets/openvpn-client.sh" %}</code></pre></div> | ||||||
|           {% endif %} |           </div> | ||||||
|  |  | ||||||
|           {% if "ikev2" in session.service.protocols %} |  | ||||||
|             <h5>StrongSwan as client</h5> |  | ||||||
|  |  | ||||||
|  |           <!-- StrongSwan as client --> | ||||||
|  |           <div class="tab-pane fade" id="snippet-strongswan" role="tabpanel" aria-labelledby="strongswan"> | ||||||
|             <p>First acquire certificates using the snippet above.</p> |             <p>First acquire certificates using the snippet above.</p> | ||||||
|  |  | ||||||
|             <p>Then install software:</p> |             <p>Then install software:</p> | ||||||
| @@ -59,12 +105,19 @@ | |||||||
|  |  | ||||||
|             <p>To configure StrongSwan as roadwarrior:</p> |             <p>To configure StrongSwan as roadwarrior:</p> | ||||||
|             <div class="highlight"><pre class="code"><code>{% include "snippets/strongswan-client.sh" %}</code></pre></div> |             <div class="highlight"><pre class="code"><code>{% include "snippets/strongswan-client.sh" %}</code></pre></div> | ||||||
|           {% endif %} |           </div> | ||||||
|  |  | ||||||
|  |           <!-- Ansible --> | ||||||
|  |           <div class="tab-pane fade" id="snippet-ansible" role="tabpanel" aria-labelledby="ansible"> | ||||||
|  |             <p>Fetch Ansible roles from https://github.com/laurivosandi/certidude-ansible</p> | ||||||
|  |             <p>In your site.yml add:</p> | ||||||
|  |             <div class="highlight"><pre class="code"><code>{% include "snippets/ansible-site.yml" %}</code></pre></div> | ||||||
|  |           </div> | ||||||
|  |  | ||||||
|           <h5>OpenWrt/LEDE as VPN gateway</h5> |           <!-- LEDE --> | ||||||
|  |           <div class="tab-pane fade" id="snippet-lede" role="tabpanel" aria-labelledby="lede"> | ||||||
|           <p>First enroll certificates using the snippet from UNIX section above</p> |             <p>To enroll from OpenWrt/LEDE and to set it up as OpenVPN/IKEv2 gateway, | ||||||
|  |             first enroll certificates using the snippet from UNIX section above</p> | ||||||
|  |  | ||||||
|             <p>Then:</p> |             <p>Then:</p> | ||||||
|             <div class="highlight"> |             <div class="highlight"> | ||||||
| @@ -92,28 +145,16 @@ | |||||||
| ipsec restart</code></pre> | ipsec restart</code></pre> | ||||||
|               </div> |               </div> | ||||||
|             {% endif %} |             {% endif %} | ||||||
|  |           </div> | ||||||
|  |  | ||||||
|           {% if session.authority.builder %} |           <!-- Copy & paste --> | ||||||
|             <h5>OpenWrt/LEDE image builder</h5> |           <div class="tab-pane fade" id="snippet-copypaste" role="tabpanel" aria-labelledby="copypaste"> | ||||||
|             <p>Hit a link to generate machine specific image. Note that this might take couple minutes to finish.</p> |  | ||||||
|             <ul> |  | ||||||
|               {% for name, title, filename in session.authority.builder.profiles %} |  | ||||||
|                 <li><a href="/api/build/{{ name }}/{{ filename }}">{{ title }}</a></li> |  | ||||||
|               {% endfor %} |  | ||||||
|             </ul> |  | ||||||
|           {% endif %} |  | ||||||
|  |  | ||||||
|           <h5>SCEP</h5> |  | ||||||
|           <p>Use following as the enrollment URL: http://{{ authority_name }}/cgi-bin/pkiclient.exe</p> |  | ||||||
|  |  | ||||||
|           <h5>Copy & paste</h5> |  | ||||||
|  |  | ||||||
|             <p>Use whatever tools you have available on your platform to generate |             <p>Use whatever tools you have available on your platform to generate | ||||||
|             keypair and just paste ASCII armored PEM file contents here and hit submit:</p> |             keypair and just paste ASCII armored PEM file contents here and hit submit:</p> | ||||||
|  |  | ||||||
|  |             <form action="/api/request/" method="post"> | ||||||
|               <textarea id="request_body" style="width:100%; min-height: 10em;" |               <textarea id="request_body" style="width:100%; min-height: 10em;" | ||||||
|                 placeholder="-----BEGIN CERTIFICATE REQUEST-----"></textarea> |                 placeholder="-----BEGIN CERTIFICATE REQUEST-----"></textarea> | ||||||
|         </div> |  | ||||||
|               <div class="modal-footer"> |               <div class="modal-footer"> | ||||||
|                 <div class="btn-group"> |                 <div class="btn-group"> | ||||||
|                   <button type="button" onclick="onSubmitRequest();" class="btn btn-primary"><i class="fa fa-upload"></i> Submit</button> |                   <button type="button" onclick="onSubmitRequest();" class="btn btn-primary"><i class="fa fa-upload"></i> Submit</button> | ||||||
| @@ -125,6 +166,10 @@ | |||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </div> | ||||||
|  |  | ||||||
| <div class="modal fade" id="revocation_list_modal" role="dialog"> | <div class="modal fade" id="revocation_list_modal" role="dialog"> | ||||||
|   <div class="modal-dialog modal-lg"> |   <div class="modal-dialog modal-lg"> | ||||||
|     <div class="modal-content"> |     <div class="modal-content"> | ||||||
| @@ -133,10 +178,10 @@ | |||||||
|         <h4 class="modal-title">Revocation lists</h4> |         <h4 class="modal-title">Revocation lists</h4> | ||||||
|       </div> |       </div> | ||||||
|       <div class="modal-body"> |       <div class="modal-body"> | ||||||
|         <p>To fetch <a href="http://{{authority_name}}/api/revoked/">certificate revocation list</a>:</p> |         <p>To fetch <a href="http://{{ session.authority.hostname }}/api/revoked/">certificate revocation list</a>:</p> | ||||||
| <pre><code>curl http://{{authority_name}}/api/revoked/ > crl.der | <pre><code>curl http://{{ session.authority.hostname }}/api/revoked/ > crl.der | ||||||
| curl http://{{authority_name}}/api/revoked/ -L -H "Accept: application/x-pem-file" | curl http://{{ session.authority.hostname }}/api/revoked/ -L -H "Accept: application/x-pem-file" | ||||||
| curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/x-pem-file" > crl.pem</code></pre> | curl http://{{ session.authority.hostname }}/api/revoked/?wait=yes -L -H "Accept: application/x-pem-file" > crl.pem</code></pre> | ||||||
|       </div> |       </div> | ||||||
|       <div class="modal-footer"> |       <div class="modal-footer"> | ||||||
|         <button type="button" class="btn" data-dismiss="modal">Close</button> |         <button type="button" class="btn" data-dismiss="modal">Close</button> | ||||||
| @@ -146,11 +191,17 @@ curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/ | |||||||
| </div> | </div> | ||||||
|  |  | ||||||
| <div class="row"> | <div class="row"> | ||||||
|   <div class="col-sm-{{ column_width }}"> |   <div class="col-sm-6 col-lg-4 col-xl-3"> | ||||||
|     <h1>Signed certificates</h1> |     <h1>Signed certificates</h1> | ||||||
|     <p>Authority administration allowed for |     <p>Authority administration | ||||||
|         {% for user in session.authority.admin_users %}<a href="mailto:{{ user.mail}}">{{ user.given_name }} {{user.surname }}</a>{% if not loop.last %}, {% endif %}{% endfor %} from {% if "0.0.0.0/0" in session.authority.admin_subnets %}anywhere{% else %} |       {% if session.authority.certificate.organization %}of {{ session.authority.certificate.organization }}{% endif %} | ||||||
|         {% for subnet in session.authority.admin_subnets %}{{ subnet }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %}. |         allowed for | ||||||
|  |         {% for user in session.authorization.admin_users %}<a href="mailto:{{ user.mail}}">{{ user.given_name }} {{user.surname }}</a>{% if not loop.last %}, {% endif %}{% endfor %} from {% if "0.0.0.0/0" in session.authorization.admin_subnets %}anywhere{% else %} | ||||||
|  |         {% for subnet in session.authorization.admin_subnets %}{{ subnet }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %}. | ||||||
|  |         Authority valid from | ||||||
|  |         <time class="timeago" datetime="{{ session.authority.certificate.signed }}">{{ session.authority.certificate.signed }}</time> | ||||||
|  |         until | ||||||
|  |         <time class="timeago" datetime="{{ session.authority.certificate.expires }}">{{ session.authority.certificate.expires }}</time>. | ||||||
|         Authority certificate can be downloaded from <a href="/api/certificate/">here</a>. |         Authority certificate can be downloaded from <a href="/api/certificate/">here</a>. | ||||||
|         Following certificates have been signed:</p> |         Following certificates have been signed:</p> | ||||||
|     <div id="signed_certificates"> |     <div id="signed_certificates"> | ||||||
| @@ -159,7 +210,7 @@ curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/ | |||||||
|     {% endfor %} |     {% endfor %} | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
|   <div class="col-sm-{{ column_width }}"> |   <div class="col-sm-6 col-lg-4 col-xl-3"> | ||||||
|   {% if session.authority %} |   {% if session.authority %} | ||||||
|     {% if session.features.token %} |     {% if session.features.token %} | ||||||
|       <h1>Tokens</h1> |       <h1>Tokens</h1> | ||||||
| @@ -174,51 +225,89 @@ curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/ | |||||||
|             <input id="token_username" name="username" type="text" class="form-control" placeholder="Username" aria-describedby="sizing-addon2"> |             <input id="token_username" name="username" type="text" class="form-control" placeholder="Username" aria-describedby="sizing-addon2"> | ||||||
|             <input id="token_mail" name="mail" type="mail" class="form-control" placeholder="Optional e-mail" aria-describedby="sizing-addon2"> |             <input id="token_mail" name="mail" type="mail" class="form-control" placeholder="Optional e-mail" aria-describedby="sizing-addon2"> | ||||||
|             <span class="input-group-btn"> |             <span class="input-group-btn"> | ||||||
|                 <button class="btn btn-secondary" type="button" onClick="onSendToken();"><i class="fa fa-send"></i> Send token</button> |                 <button class="btn btn-secondary" type="button" onClick="onIssueToken();"><i class="fa fa-send"></i> Send token</button> | ||||||
|             </span> |             </span> | ||||||
|         </div> |         </div> | ||||||
|       </p> |       </p> | ||||||
|  |  | ||||||
|  |       <p>Issued tokens:</p> | ||||||
|  |       <ul> | ||||||
|  |         {% for token in session.authority.tokens %} | ||||||
|  |           <li> | ||||||
|  |           <a href="mailto:{{ token.mail }}">{{ token.subject }}</a> | ||||||
|  |           {% if token.issuer %}{% if token.issuer != token.subject %}by {{ token.issuer }}{% else %}by himself{% endif %}{% else %}via shell{% endif %}, | ||||||
|  |           expires | ||||||
|  |           <time class="timeago" datetime="{{ token.expires }}">{{ token.expires }}</time> | ||||||
|  |           </li> | ||||||
|  |         {% endfor %} | ||||||
|  |       </ul> | ||||||
|  |  | ||||||
|       <div id="token_qrcode"></div> |       <div id="token_qrcode"></div> | ||||||
|     {% endif %} |     {% endif %} | ||||||
|  |  | ||||||
|  |     {% if session.authorization.request_subnets %} | ||||||
|       <h1>Pending requests</h1> |       <h1>Pending requests</h1> | ||||||
|  |  | ||||||
|       <p>Use Certidude client to apply for a certificate. |       <p>Use Certidude client to apply for a certificate. | ||||||
|  |  | ||||||
|     {% if not session.authority.request_subnets %} |       {% if not session.authorization.request_subnets %} | ||||||
|          Request submission disabled. |          Request submission disabled. | ||||||
|     {% elif "0.0.0.0/0" in session.authority.request_subnets %} |       {% elif "0.0.0.0/0" in session.authorization.request_subnets %} | ||||||
|          Request submission is enabled. |          Request submission is enabled. | ||||||
|       {% else %} |       {% else %} | ||||||
|          Request submission allowed from |          Request submission allowed from | ||||||
|        {% for subnet in session.authority.request_subnets %} |          {% for subnet in session.authorization.request_subnets %} | ||||||
|            {{ subnet }}{% if not loop.last %}, {% endif %} |            {{ subnet }}{% if not loop.last %}, {% endif %} | ||||||
|          {% endfor %}. |          {% endfor %}. | ||||||
|       {% endif %} |       {% endif %} | ||||||
|  |  | ||||||
|       See <a href="#request_submission_modal" data-toggle="modal">here</a> for more information on manual signing request upload. |       See <a href="#request_submission_modal" data-toggle="modal">here</a> for more information on manual signing request upload. | ||||||
|  |  | ||||||
|     {% if session.authority.autosign_subnets %} |       {% if session.authorization.autosign_subnets %} | ||||||
|         {% if "0.0.0.0/0" in session.authority.autosign_subnets %} |           {% if "0.0.0.0/0" in session.authorization.autosign_subnets %} | ||||||
|               All requests are automatically signed. |               All requests are automatically signed. | ||||||
|           {% else %} |           {% else %} | ||||||
|              Requests from |              Requests from | ||||||
|                 {% for subnet in session.authority.autosign_subnets %} |                   {% for subnet in session.authorization.autosign_subnets %} | ||||||
|                       {{ subnet }}{% if not loop.last %}, {% endif %} |                       {{ subnet }}{% if not loop.last %}, {% endif %} | ||||||
|                   {% endfor %} |                   {% endfor %} | ||||||
|              are automatically signed. |              are automatically signed. | ||||||
|           {% endif %} |           {% endif %} | ||||||
|       {% endif %} |       {% endif %} | ||||||
|  |  | ||||||
|  |       {% if session.authorization.scep_subnets %} | ||||||
|  |         To enroll via SCEP from | ||||||
|  |         {% if "0.0.0.0/0" in session.authorization.scep_subnets %} | ||||||
|  |         anywhere | ||||||
|  |         {% else %} | ||||||
|  |           {% for subnet in session.authorization.scep_subnets %} | ||||||
|  |             {{ subnet }}{% if not loop.last %}, {% endif %} | ||||||
|  |           {% endfor %} | ||||||
|  |         {% endif %} | ||||||
|  |         use http://{{ session.authority.hostname }}/cgi-bin/pkiclient.exe as the enrollment URL. | ||||||
|  |       {% endif %} | ||||||
|  |  | ||||||
|       </p> |       </p> | ||||||
|       <div id="pending_requests"> |       <div id="pending_requests"> | ||||||
|         {% for request in session.authority.requests | sort(attribute="submitted", reverse=true) %} |         {% for request in session.authority.requests | sort(attribute="submitted", reverse=true) %} | ||||||
|           {% include "views/request.html" %} |           {% include "views/request.html" %} | ||||||
|         {% endfor %} |         {% endfor %} | ||||||
|       </div> |       </div> | ||||||
|   {% if columns >= 3 %} |  | ||||||
|   </div> |  | ||||||
|   <div class="col-sm-{{ column_width }}"> |  | ||||||
|     {% endif %} |     {% endif %} | ||||||
|  |  | ||||||
|  |     {% if session.builder.profiles %} | ||||||
|  |       <h2>LEDE imagebuilder</h2> | ||||||
|  |       <p>Hit a link to generate machine specific image. Note that this might take couple minutes to finish.</p> | ||||||
|  |       <ul> | ||||||
|  |         {% for name, title, filename in session.builder.profiles %} | ||||||
|  |           <li><a href="/api/build/{{ name }}/{{ filename }}">{{ title }}</a></li> | ||||||
|  |         {% endfor %} | ||||||
|  |       </ul> | ||||||
|  |     {% endif %} | ||||||
|  |  | ||||||
|  |   </div> | ||||||
|  |   <div class="col-sm-6 col-lg-4 col-xl-3"> | ||||||
|  |  | ||||||
|     <h1>Revoked certificates</h1> |     <h1>Revoked certificates</h1> | ||||||
|     <p>Following certificates have been revoked{% if session.features.crl %}, for more information click |     <p>Following certificates have been revoked{% if session.features.crl %}, for more information click | ||||||
|     <a href="#revocation_list_modal" data-toggle="modal">here</a>{% endif %}.</p> |     <a href="#revocation_list_modal" data-toggle="modal">here</a>{% endif %}.</p> | ||||||
| @@ -227,7 +316,7 @@ curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/ | |||||||
|       {% include "views/revoked.html" %} |       {% include "views/revoked.html" %} | ||||||
|     {% endfor %} |     {% endfor %} | ||||||
|   </div> |   </div> | ||||||
|   <div id="column-log" class="col-sm-{% if columns == 4 %}{{ column_width }}{% else %}12{% endif %}" {% if columns < 4 %}style="display:none;"{% endif %}> |   <div id="column-log" class="col-sm-6 col-lg-4 col-xl-3 hidden-lg-down"> | ||||||
|     <div class="loader-container"> |     <div class="loader-container"> | ||||||
|       <div class="loader"></div> |       <div class="loader"></div> | ||||||
|       <p>Loading logs, this might take a while...</p> |       <p>Loading logs, this might take a while...</p> | ||||||
| @@ -236,13 +325,14 @@ curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/ | |||||||
|       <h1>Log</h1> |       <h1>Log</h1> | ||||||
|       <div class="btn-group" data-toggle="buttons"> |       <div class="btn-group" data-toggle="buttons"> | ||||||
|         <label class="btn btn-primary active"><input id="log-level-critical" type="checkbox" autocomplete="off" checked>Critical</label> |         <label class="btn btn-primary active"><input id="log-level-critical" type="checkbox" autocomplete="off" checked>Critical</label> | ||||||
|         <label class="btn btn-primary active"><input id="log-level-errors" type="checkbox" autocomplete="off" checked> Errors</label> |         <label class="btn btn-primary active"><input id="log-level-error" type="checkbox" autocomplete="off" checked>Error</label> | ||||||
|         <label class="btn btn-primary active"><input id="log-level-warnings" type="checkbox" autocomplete="off" checked> Warnings</label> |         <label class="btn btn-primary active"><input id="log-level-warning" type="checkbox" autocomplete="off" checked>Warn</label> | ||||||
|         <label class="btn btn-primary active"><input id="log-level-info" type="checkbox" autocomplete="off" checked>Info</label> |         <label class="btn btn-primary active"><input id="log-level-info" type="checkbox" autocomplete="off" checked>Info</label> | ||||||
|         <label class="btn btn-primary"><input id="log-level-debug" type="checkbox" autocomplete="off">Debug</label> |         <label class="btn btn-primary"><input id="log-level-debug" type="checkbox" autocomplete="off">Debug</label> | ||||||
|       </div> |       </div> | ||||||
|       <ul id="log-entries" class="list-group"> |       <ul id="log-entries" class="list-group"> | ||||||
|       </ul> |       </ul> | ||||||
|  |       <p>Click here to load more entries</p> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
| </div> | </div> | ||||||
|   | |||||||
							
								
								
									
										238
									
								
								certidude/static/views/enroll.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,238 @@ | |||||||
|  | <!-- https://wiki.strongswan.org/projects/strongswan/wiki/AppleIKEv2Profile#Certificate-authentication --> | ||||||
|  |  | ||||||
|  | <!-- | ||||||
|  |  | ||||||
|  | Browser status | ||||||
|  |  | ||||||
|  | - Edge doesn't work because they think data: urls are insecure | ||||||
|  | - iphone QR code scanner's webview is constrained, cant download data: links | ||||||
|  | - outlook.com via iphone mail client works | ||||||
|  | - android gmail app works | ||||||
|  | - chrome works | ||||||
|  | - firefox works | ||||||
|  |  | ||||||
|  | OS/soft status | ||||||
|  |  | ||||||
|  | - OpenVPN works on everything | ||||||
|  | - StrongSwan app works on Android | ||||||
|  | - NetworkManager doesn't support importing .sswan files yet, so no IPSec support for Ubuntu or Fedora here yet | ||||||
|  |  | ||||||
|  | --> | ||||||
|  |  | ||||||
|  | <div id="enroll" class="row"> | ||||||
|  |   <div class="loader-container"> | ||||||
|  |     <div class="loader"></div> | ||||||
|  |     <p>Generating RSA keypair, this will take a while...</p> | ||||||
|  |   </div> | ||||||
|  |  | ||||||
|  |   <div class="col-sm-12 mt-3 edge-broken" style="display:none;"> | ||||||
|  |     <!-- https://stackoverflow.com/questions/33154646/data-uri-link-a-href-data-doesnt-work-in-microsoft-edge?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa --> | ||||||
|  |     Microsoft Edge not supported, open the link with Chrome or Firefox | ||||||
|  |   </div> | ||||||
|  |  | ||||||
|  |   <div class="col-sm-12 mt-3 option ubuntu linux openvpn"> | ||||||
|  |     <div class="card"> | ||||||
|  |       <div class="card-block"> | ||||||
|  |         <h3 class="card-title">Ubuntu 16.04+</h3> | ||||||
|  |         <p class="card-text">Install OpenVPN plugin for NetworkManager by executing following two command in the terminal: | ||||||
|  |  | ||||||
|  |         <pre><code># Ubuntu 16.04 ships with older OpenVPN 2.3, to support newer ciphers add OpenVPN's repo | ||||||
|  | if [ $(lsb_relase -cs) == "xenial" ]; then | ||||||
|  |   wget -O - https://swupdate.openvpn.net/repos/repo-public.gpg|apt-key add - | ||||||
|  |   echo "deb http://build.openvpn.net/debian/openvpn/release/2.4 xenial main" > /etc/apt/sources.list.d/openvpn-aptrepo.list | ||||||
|  |   apt update | ||||||
|  |   apt install openvpn | ||||||
|  | fi | ||||||
|  |  | ||||||
|  | sudo apt install -y network-manager-openvpn-gnome | ||||||
|  | sudo systemctl restart network-manager | ||||||
|  | </code></pre> | ||||||
|  |  | ||||||
|  |         <p> | ||||||
|  |           <a href="javascript:onEnroll('ovpn');" class="btn btn-primary">Fetch OpenVPN profile</a> | ||||||
|  |           <button class="btn btn-secondary" type="button" data-toggle="collapse" data-target="#ubuntu-screenshots" aria-expanded="false" aria-controls="ubuntu-screenshots"> | ||||||
|  |             Screenshots | ||||||
|  |           </button> | ||||||
|  |         </p> | ||||||
|  |  | ||||||
|  |         <div class="collapse" id="ubuntu-screenshots"> | ||||||
|  |             <p>Open up network connections:</p> | ||||||
|  |             <p><img src="/img/ubuntu-01-edit-connections.png"/></p> | ||||||
|  |             <p>Hit <i>Add button</i>:</p> | ||||||
|  |             <p><img src="/img/ubuntu-02-network-connections.png"/></p> | ||||||
|  |             <p>Select <i>Import a saved VPN configuration...</i>:</p> | ||||||
|  |             <p><img src="/img/ubuntu-03-import-saved-config.png"/></p> | ||||||
|  |             <p>Select downloaded file:</p> | ||||||
|  |             <p><img src="/img/ubuntu-04-select-file.png"/></p> | ||||||
|  |             <p>Once profile is successfully imported following dialog appears:</p> | ||||||
|  |             <p><img src="/img/ubuntu-05-profile-imported.png"/></p> | ||||||
|  |             <p>By default all traffic is routed via VPN gateway, route only intranet subnets to the gateway select <i>Routes...</i> under <i>IPv4 Settings</i>:</p> | ||||||
|  |             <p><img src="/img/ubuntu-06-ipv4-settings.png"/></p> | ||||||
|  |             <p>Check <i>Use this connection only for resources on its network</i>:</p> | ||||||
|  |             <p><img src="/img/ubuntu-07-disable-default-route.png"/></p> | ||||||
|  |             <p>To activate the connection select it under <i>VPN Connections</i>:</p> | ||||||
|  |             <p><img src="/img/ubuntu-08-activate-connection.png"/></p> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   <div class="col-sm-12 mt-3 option fedora linux openvpn"> | ||||||
|  |     <div class="card"> | ||||||
|  |       <div class="card-block"> | ||||||
|  |         <h3 class="card-title">Fedora</h3> | ||||||
|  |         <p class="card-text">Install OpenVPN plugin for NetworkManager by running following two commands:</p> | ||||||
|  |         <pre><code>dnf install NetworkManager-openvpn-gnome | ||||||
|  | systemctl restart NetworkManager</code></pre> | ||||||
|  |           Right click in the NetworkManager icon, select network settings. Hit the + button and select <i>Import from file...</i>, select the downloaded .ovpn file. | ||||||
|  |           Remove the .ovpn file from the Downloads folder.</p> | ||||||
|  |         <a href="javascript:onEnroll('ovpn');" class="btn btn-primary">Fetch OpenVPN profile</a> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  |  | ||||||
|  |   <div class="col-sm-12 mt-3 option windows ipsec"> | ||||||
|  |     <div class="card"> | ||||||
|  |       <div class="card-block"> | ||||||
|  |         <h3 class="card-title">Windows</h3> | ||||||
|  |         <p class="card-text"> | ||||||
|  |           Import PKCS#12 container to your machine trust store. | ||||||
|  |           Import VPN connection profile by moving the downloaded .pbk file to | ||||||
|  |         <pre><code>%userprofile%\AppData\Roaming\Microsoft\Network\Connections\PBK</code></pre> | ||||||
|  |         or | ||||||
|  |         <pre><code>C:\ProgramData\Microsoft\Network\Connections\Pbk</code></pre></p> | ||||||
|  |         <a href="javascript:onEnroll('p12');" class="btn btn-primary">Fetch PKCS#12 container</a> | ||||||
|  |         <a href="#" class="btn btn-secondary">Fetch VPN profile</a> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  |  | ||||||
|  |   <div class="col-sm-12 mt-3 option windows openvpn"> | ||||||
|  |     <div class="card"> | ||||||
|  |       <div class="card-block"> | ||||||
|  |         <h3 class="card-title">Windows</h3> | ||||||
|  |         <p class="card-text"> | ||||||
|  |           Install OpenVPN community edition client. | ||||||
|  |           Move the downloaded .ovpn file to C:\Program Files\OpenVPN\config and | ||||||
|  |           right click in the system tray on OpenVPN icon and select Connect from the menu. | ||||||
|  |           For finishing touch adjust the file permissions so only local | ||||||
|  |           administrator can read that file, remove regular user access to the file. | ||||||
|  |         </p> | ||||||
|  |         <a href="https://openvpn.net/index.php/download/community-downloads.html" class="btn btn-secondary">Get OpenVPN community edition</a> | ||||||
|  |         <a href="javascript:onEnroll('ovpn');" class="btn btn-primary">Fetch OpenVPN profile</a> | ||||||
|  |         <button class="btn btn-secondary" type="button" data-toggle="collapse" data-target="#windows-screenshots" aria-expanded="false" aria-controls="windows-screenshots"> | ||||||
|  |             Screenshots | ||||||
|  |          </button> | ||||||
|  |  | ||||||
|  |         <div class="collapse" id="windows-screenshots"> | ||||||
|  |           <p>Download OpenVPN from the link supplied above:</p> | ||||||
|  |           <p><img src="/img/windows-01-download-openvpn.png"/></p> | ||||||
|  |  | ||||||
|  |           <p>Install OpenVPN:</p> | ||||||
|  |           <p><img src="/img/windows-02-install-openvpn.png"/></p> | ||||||
|  |  | ||||||
|  |           <p>Move the configuraiton file downloaded from the second button above:</p> | ||||||
|  |           <p><img src="/img/windows-03-move-config-file.png"/></p> | ||||||
|  |  | ||||||
|  |           <p>Connect from system tray:</p> | ||||||
|  |           <p><img src="/img/windows-04-connect.png"/></p> | ||||||
|  |  | ||||||
|  |           <p>Connection is successfully configured:</p> | ||||||
|  |           <p><img src="/img/windows-05-connected.png"/></p> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  |  | ||||||
|  |   <div class="col-sm-12 mt-3 option mac openvpn"> | ||||||
|  |     <div class="card"> | ||||||
|  |       <div class="card-block"> | ||||||
|  |         <h3 class="card-title">Mac OS X</h3> | ||||||
|  |         <p class="card-text">Download Tunnelblick. Tap on the button above and import the profile.</p> | ||||||
|  |         <a  href="https://tunnelblick.net/" target="_blank" class="btn btn-secondary">Get Tunnelblick</a> | ||||||
|  |         <a href="javascript:onEnroll('ovpn');" class="btn btn-primary">Fetch OpenVPN profile</a> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  |  | ||||||
|  |   <div class="col-sm-12 mt-3 option iphone ipad openvpn"> | ||||||
|  |     <div class="card"> | ||||||
|  |       <div class="card-block"> | ||||||
|  |         <h3 class="card-title">iPhone/iPad</h3> | ||||||
|  |         <p class="card-text">Install OpenVPN Connect app, tap on the button below.</p> | ||||||
|  |         <a href="https://itunes.apple.com/us/app/openvpn-connect/id590379981?mt=8" target="_blank" class="btn btn-secondary">Get OpenVPN Connect app</a> | ||||||
|  |         <a href="javascript:onEnroll('ovpn');" class="btn btn-primary">Fetch OpenVPN profile</a> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  |  | ||||||
|  |   <div class="col-sm-12 mt-3 option iphone ipad ikev2"> | ||||||
|  |     <div class="card"> | ||||||
|  |       <div class="card-block"> | ||||||
|  |         <h3 class="card-title">iPhone/iPad</h3> | ||||||
|  |         <p class="card-text"> | ||||||
|  |           Tap the button below, you'll be prompted about configuration profile, tap <i>Allow</i>. | ||||||
|  |           Hit <i>Install</i> in the top-right corner. | ||||||
|  |           Enter your passcode to unlock trust store. | ||||||
|  |           Tap <i>Install</i> and confirm by hitting <i>Install</i>. | ||||||
|  |           Where password for the certificate is prompted, enter 1234. | ||||||
|  |           Hit <i>Done</i>. Go to <i>Settings</i>, open VPN submenu and tap on the VPN profile to connect. | ||||||
|  |         </p> | ||||||
|  |         <a href="javascript:onEnroll('mobileconfig');" class="btn btn-primary">Fetch VPN profile</a> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  |  | ||||||
|  |   <div class="col-sm-12 mt-3 option mac ikev2"> | ||||||
|  |     <div class="card"> | ||||||
|  |       <div class="card-block"> | ||||||
|  |         <h3 class="card-title">Mac OS X</h3> | ||||||
|  |         <p class="card-text"> | ||||||
|  |           Click on the button below, you'll be prompted about configuration profile, tap <i>Allow</i>. | ||||||
|  |           Hit <i>Install</i> in the top-right corner. | ||||||
|  |           Enter your passcode to unlock trust store. | ||||||
|  |           Tap <i>Install</i> and confirm by hitting <i>Install</i>. | ||||||
|  |           Where password for the certificate is prompted, enter 1234. | ||||||
|  |           Hit <i>Done</i>. Go to <i>Settings</i>, open VPN submenu and tap on the VPN profile to connect. | ||||||
|  |         </p> | ||||||
|  |         <a href="javascript:onEnroll('mobileconfig');" class="btn btn-primary">Fetch VPN profile</a> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  |  | ||||||
|  |   <div class="col-sm-12 mt-3 option android openvpn"> | ||||||
|  |     <div class="card"> | ||||||
|  |       <div class="card-block"> | ||||||
|  |         <h3 class="card-title">Android</h3> | ||||||
|  |         <p class="card-text">Intall OpenVPN Connect app on your device. | ||||||
|  |           Tap on the downloaded .ovpn file, OpenVPN Connect should prompt for import. | ||||||
|  |           Hit <i>Accept</i> and then <i>Connect</i>. | ||||||
|  |           Remember to delete any remaining .ovpn files under the <i>Downloads</i>. | ||||||
|  |         </p> | ||||||
|  |         <a href="https://play.google.com/store/apps/details?id=net.openvpn.openvpn" target="_blank" class="btn btn-secondary">Get OpenVPN Connect app</a> | ||||||
|  |         <a href="javascript:onEnroll('ovpn');" class="btn btn-primary">Fetch OpenVPN profile</a> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  |  | ||||||
|  |   <div class="col-sm-12 mt-3 option android ikev2"> | ||||||
|  |     <div class="card"> | ||||||
|  |       <div class="card-block"> | ||||||
|  |         <h3 class="card-title">Android</h3> | ||||||
|  |         <p class="card-text"> | ||||||
|  |           Install strongSwan Client app on your device. | ||||||
|  |           Tap on the downloaded .sswan file, StrongSwan Client should prompt for import. | ||||||
|  |           Hit <i>Import certificate from VPN profile</i> and then <i>Import</i> in the top-right corner. | ||||||
|  |           Remember to delete any remaining .sswan files under the <i>Downloads</i>. | ||||||
|  |         </p> | ||||||
|  |         <a href="https://play.google.com/store/apps/details?id=org.strongswan.android" class="btn btn-secondary">Get strongSwan VPN Client app</a> | ||||||
|  |         <a href="javascript:onEnroll('sswan');" class="btn btn-primary">Fetch StrongSwan profile</a> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  |   <!-- | ||||||
|  |     <a href="javascript:onShowAll();">I did't find an appropriate option for me, show all options</a> | ||||||
|  |   --> | ||||||
|  |  | ||||||
|  | </div> | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| <p>You're viewing this page over insecure channel. | <p>You're viewing this page over insecure channel. | ||||||
|   You can give it a try and <a href="https://{{ authority_name }}">connect over HTTPS</a>, |   You can give it a try and <a href="https://{{ session.authority.hostname }}">connect over HTTPS</a>, | ||||||
|   if that succeeds all subsequents accesses of this page will go over HTTPS. |   if that succeeds all subsequents accesses of this page will go over HTTPS. | ||||||
| </p> | </p> | ||||||
| <p> | <p> | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| <li id="log_entry_{{ entry.id }}" class="list-group-item justify-content-between filterable{% if entry.fresh %} fresh{% endif %}"> | <li id="log_entry_{{ entry.id }}" data-keywords="{{ entry.message }}" class="list-group-item justify-content-between filterable{% if entry.fresh %} fresh{% endif %}"> | ||||||
| <span> | <span> | ||||||
| <i class="fa fa-{{ entry.severity }}-circle"/> | <i class="fa fa-{{ entry.severity }}-circle"/> | ||||||
| {{ entry.message }} | {{ entry.message }} | ||||||
|   | |||||||
| @@ -40,8 +40,8 @@ | |||||||
|     <div class="collapse" id="details-{{ request.sha256sum }}"> |     <div class="collapse" id="details-{{ request.sha256sum }}"> | ||||||
|       <p>Use following to fetch the signing request:</p> |       <p>Use following to fetch the signing request:</p> | ||||||
|       <div class="bd-example"> |       <div class="bd-example"> | ||||||
|         <pre><code class="language-sh" data-lang="sh">wget <a href="/api/request/{{ request.common_name }}/">http://{{ window.location.hostname }}/api/request/{{ request.common_name }}/</a> |         <pre><code class="language-sh" data-lang="sh">wget <a href="/api/request/{{ request.common_name }}/">http://{{ session.authority.hostname }}/api/request/{{ request.common_name }}/</a> | ||||||
| curl http://{{ window.location.hostname }}/api/request/{{ request.common_name }}/ \ | curl http://{{ session.authority.hostname }}/api/request/{{ request.common_name }}/ \ | ||||||
|   | openssl req -text -noout</code></pre> |   | openssl req -text -noout</code></pre> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -29,15 +29,15 @@ | |||||||
|         <p>To fetch certificate:</p> |         <p>To fetch certificate:</p> | ||||||
|  |  | ||||||
|         <div class="bd-example"> |         <div class="bd-example"> | ||||||
|           <pre><code class="language-sh" data-lang="sh">wget <a href="/api/revoked/{{ certificate.serial }}/">http://{{ window.location.hostname }}/api/revoked/{{ certificate.serial }}/</a> |           <pre><code class="language-sh" data-lang="sh">wget <a href="/api/revoked/{{ certificate.serial }}/">http://{{ session.authority.hostname }}/api/revoked/{{ certificate.serial }}/</a> | ||||||
| curl http://{{ window.location.hostname }}/api/revoked/{{ certificate.serial }}/ \ | curl http://{{ session.authority.hostname }}/api/revoked/{{ certificate.serial }}/ \ | ||||||
|   | openssl x509 -text -noout</code></pre> |   | openssl x509 -text -noout</code></pre> | ||||||
|         </div> |         </div> | ||||||
|  |  | ||||||
|         <p>To perform online certificate status request</p> |         <p>To perform online certificate status request</p> | ||||||
|         <pre><code class="language-bash" data-lang="bash">curl http://{{ window.location.hostname }}/api/certificate/ > session.pem |         <pre><code class="language-bash" data-lang="bash">curl http://{{ session.authority.hostname }}/api/certificate/ > session.pem | ||||||
| openssl ocsp -issuer session.pem -CAfile session.pem \ | openssl ocsp -issuer session.pem -CAfile session.pem \ | ||||||
|   -url http://{{ window.location.hostname }}/api/ocsp/ \ |   -url http://{{ session.authority.hostname }}/api/ocsp/ \ | ||||||
|   -serial 0x{{ certificate.serial }}</span></code></pre> |   -serial 0x{{ certificate.serial }}</span></code></pre> | ||||||
|  |  | ||||||
|         <p> |         <p> | ||||||
|   | |||||||
| @@ -56,7 +56,7 @@ | |||||||
|  |  | ||||||
|     <div class="btn-group"> |     <div class="btn-group"> | ||||||
|       {% if session.authority.tagging %} |       {% if session.authority.tagging %} | ||||||
|         <button type="button" class="btn btn-default" onclick="onNewTagClicked(this);" data-key="other" data-cn="{{ certificate.common_name }}"> |         <button type="button" class="btn btn-default" onclick="onNewTagClicked(event);" data-key="other" data-cn="{{ certificate.common_name }}"> | ||||||
|          <i class="fa fa-tag"></i> Tag</button> |          <i class="fa fa-tag"></i> Tag</button> | ||||||
|         <button type="button" class="btn btn-default dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> |         <button type="button" class="btn btn-default dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> | ||||||
|           <span class="sr-only">Toggle Dropdown</span> |           <span class="sr-only">Toggle Dropdown</span> | ||||||
| @@ -64,7 +64,7 @@ | |||||||
|         <div class="dropdown-menu"> |         <div class="dropdown-menu"> | ||||||
|           {% for tag_category in session.authority.tagging %} |           {% for tag_category in session.authority.tagging %} | ||||||
|           <a class="dropdown-item" href="#" data-key="{{ tag_category.name }}" data-cn="{{ certificate.common_name }}" |           <a class="dropdown-item" href="#" data-key="{{ tag_category.name }}" data-cn="{{ certificate.common_name }}" | ||||||
|             onclick="onNewTagClicked(this);">{{ tag_category.title }}</a> |             onclick="onNewTagClicked(event);">{{ tag_category.title }}</a> | ||||||
|           {% endfor %} |           {% endfor %} | ||||||
|         </div> |         </div> | ||||||
|       {% endif %} |       {% endif %} | ||||||
| @@ -74,24 +74,29 @@ | |||||||
|       <p>To fetch certificate:</p> |       <p>To fetch certificate:</p> | ||||||
|  |  | ||||||
|       <div class="bd-example"> |       <div class="bd-example"> | ||||||
|         <pre><code class="language-sh" data-lang="sh">wget <a href="/api/signed/{{ certificate.common_name }}/">http://{{ window.location.hostname }}/api/signed/{{ certificate.common_name }}/</a> |         <pre><code class="language-sh" data-lang="sh">wget <a href="/api/signed/{{ certificate.common_name }}/">http://{{ session.authority.hostname }}/api/signed/{{ certificate.common_name }}/</a> | ||||||
| curl http://{{ window.location.hostname }}/api/signed/{{ certificate.common_name }}/ \ | curl --cert-status http://{{ session.authority.hostname }}/api/signed/{{ certificate.common_name }}/ \ | ||||||
|   | openssl x509 -text -noout</code></pre> |   | openssl x509 -text -noout</code></pre> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       {% if session.features.ocsp %} |       {% if session.authorization.ocsp_subnets %} | ||||||
|       <p>To perform online certificate status request:</p> |       {% if certificate.responder_url %} | ||||||
|       <pre><code class="language-bash" data-lang="bash">curl http://{{ window.location.hostname }}/api/certificate/ > session.pem |       <p>To perform online certificate status request{% if "0.0.0.0/0" not in session.authorization.ocsp_subnets %} | ||||||
|  |       from whitelisted {{ session.authorization.ocsp_subnets }} subnets{% endif %}:</p> | ||||||
|  |       <pre><code class="language-bash" data-lang="bash">curl http://{{ session.authority.hostname }}/api/certificate > session.pem | ||||||
| openssl ocsp -issuer session.pem -CAfile session.pem \ | openssl ocsp -issuer session.pem -CAfile session.pem \ | ||||||
|   -url http://{{ window.location.hostname }}/api/ocsp/ \ |   -url {{ certificate.responder_url }} \ | ||||||
|   -serial 0x{{ certificate.serial }}</code></pre> |   -serial 0x{{ certificate.serial }}</code></pre> | ||||||
|  |       {% else %} | ||||||
|  |       <p>Querying OCSP responder disabled for this certificate, see /etc/certidude/profile.conf how to enable if that's desired</p> | ||||||
|  |       {% endif %} | ||||||
|       {% endif %} |       {% endif %} | ||||||
|  |  | ||||||
|       <p>To fetch script:</p> |       <p>To fetch script:</p> | ||||||
|       <pre><code class="language-bash" data-lang="bash">curl https://{{ window.location.hostname }}:8443/api/signed/{{ certificate.common_name }}/script/ \ |       <pre><code class="language-bash" data-lang="bash">curl --cert-status https://{{ session.authority.hostname }}:8443/api/signed/{{ certificate.common_name }}/script/ \ | ||||||
|     --cacert /etc/certidude/authority/{{ window.location.hostname }}/ca_cert.pem \ |     --cacert /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem \ | ||||||
|     --key /etc/certidude/authority/{{ window.location.hostname }}/host_key.pem \ |     --key /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem \ | ||||||
|     --cert /etc/certidude/authority/{{ window.location.hostname }}/host_cert.pem</pre></code> |     --cert /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem</pre></code> | ||||||
|  |  | ||||||
|       <div style="overflow: auto; max-width: 100%;"> |       <div style="overflow: auto; max-width: 100%;"> | ||||||
|         <table class="table" id="signed_certificates"> |         <table class="table" id="signed_certificates"> | ||||||
| @@ -112,8 +117,11 @@ openssl ocsp -issuer session.pem -CAfile session.pem \ | |||||||
|             <tr><th>SHA1</th><td>{{ certificate.sha1sum }}</td></tr> |             <tr><th>SHA1</th><td>{{ certificate.sha1sum }}</td></tr> | ||||||
|             --> |             --> | ||||||
|             <tr><th>SHA256</th><td style="word-wrap:break-word;  overflow-wrap: break-word;  ">{{ certificate.sha256sum }}</td></tr> |             <tr><th>SHA256</th><td style="word-wrap:break-word;  overflow-wrap: break-word;  ">{{ certificate.sha256sum }}</td></tr> | ||||||
|             {% if certificate.extensions.extended_key_usage %} |             {% if certificate.key_usage %} | ||||||
|             <tr><th>Extended key usage</th><td>{{ certificate.extensions.extended_key_usage | join(", ") }}</td></tr> |             <tr><th>Key usage</th><td>{{ certificate.key_usage | join(", ") | replace("_", " ") }}</td></tr> | ||||||
|  |             {% endif %} | ||||||
|  |             {% if certificate.extended_key_usage %} | ||||||
|  |             <tr><th>Extended key usage</th><td>{{ certificate.extended_key_usage | join(", ") | replace("_", " ") }}</td></tr> | ||||||
|             {% endif %} |             {% endif %} | ||||||
|           </tbody> |           </tbody> | ||||||
|         </table> |         </table> | ||||||
|   | |||||||
| @@ -2,5 +2,5 @@ | |||||||
|   <span data-cn="{{ certificate.common_name }}" |   <span data-cn="{{ certificate.common_name }}" | ||||||
|     title="{{ tag.id }}" |     title="{{ tag.id }}" | ||||||
|     class="badge badge-default" |     class="badge badge-default" | ||||||
|     onClick="onTagClicked(this);">{{ tag.value }}</span> |     onClick="onTagClicked(event);">{{ tag.value }}</span> | ||||||
| {% endfor %} | {% endfor %} | ||||||
|   | |||||||
| @@ -1,9 +1,9 @@ | |||||||
| Token for {{ user.name }} | Token for {{ subject }} | ||||||
|  |  | ||||||
| {% if issuer == user %} | {% if issuer == subject %} | ||||||
| Token has been issued for {{ user }} for retrieving profile from link below. | Token has been issued for {{ subject }} for retrieving profile from link below. | ||||||
| {% else %} | {% else %} | ||||||
| {{ issuer }} has provided {{ user }} a token for retrieving | {{ issuer }} has provided {{ subject }} a token for retrieving | ||||||
| profile from the link below. | profile from the link below. | ||||||
| {% endif %} | {% endif %} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,9 +1,24 @@ | |||||||
|  | # Configure secure defaults for nginx | ||||||
|  | ssl_dhparam {{ dhparam_path }}; | ||||||
|  |  | ||||||
|  | # Note that depending on the certificate type (RSA, ECDSA) this will be even further constrained: | ||||||
|  | ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA512:DHE-ECDSA-AES256-GCM-SHA512:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384; | ||||||
|  |  | ||||||
|  | ssl_ecdh_curve secp384r1; | ||||||
|  | ssl_session_timeout  10m; | ||||||
|  | ssl_session_cache shared:SSL:10m; | ||||||
|  | ssl_session_tickets off; | ||||||
|  | ssl_trusted_certificate {{ ca_cert }}; # OCSP responder trust chain | ||||||
|  | ssl_stapling on; | ||||||
|  | ssl_stapling_verify on; | ||||||
|  | add_header X-Frame-Options DENY; | ||||||
|  | add_header X-Content-Type-Options nosniff; | ||||||
|  | add_header X-XSS-Protection "1; mode=block"; | ||||||
|  | add_header X-Robots-Tag none; | ||||||
|  |  | ||||||
| # Following are already enabled by /etc/nginx/nginx.conf | # Following are already enabled by /etc/nginx/nginx.conf | ||||||
| #ssl_protocols  TLSv1 TLSv1.1 TLSv1.2; | #ssl_protocols  TLSv1 TLSv1.1 TLSv1.2; | ||||||
| #ssl_prefer_server_ciphers on; | #ssl_prefer_server_ciphers on; | ||||||
| ssl_session_cache shared:SSL:10m; |  | ||||||
| ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; |  | ||||||
| ssl_dhparam {{dhparam_path}}; |  | ||||||
|  |  | ||||||
| # Add SSLUserName SSL_CLIENT_S_DN_CN style parameter support | # Add SSLUserName SSL_CLIENT_S_DN_CN style parameter support | ||||||
| map $ssl_client_s_dn  $ssl_client_s_dn_cn { | map $ssl_client_s_dn  $ssl_client_s_dn_cn { | ||||||
|   | |||||||
| @@ -1,13 +1,16 @@ | |||||||
| [DEFAULT] | [DEFAULT] | ||||||
|  | # LEDE image builder profiles enabled by default | ||||||
|  | enabled = yes | ||||||
|  |  | ||||||
| # Path to filesystem overlay used | # Path to filesystem overlay used | ||||||
| overlay = {{ doc_path }}/overlay | overlay = {{ doc_path }}/overlay | ||||||
|  |  | ||||||
| # Hostname or regex to match the IPSec gateway included in the image | # Hostname or regex to match the IPSec gateway included in the image | ||||||
| router = ^router\d?\. | router = ^(router|vpn|gw|gateway)\d*\. | ||||||
|  |  | ||||||
| # Site specific script to be copied to /etc/uci-defaults/99-site-script | # Site specific script to be copied to /etc/uci-defaults/99-site-script | ||||||
| # use it to include SSH keys, set passwords, etc | # use it to include SSH keys, set passwords, etc | ||||||
| script = | script = /etc/certidude/script/site.sh | ||||||
|  |  | ||||||
| # Which subnets are routed to the tunnel | # Which subnets are routed to the tunnel | ||||||
| subnets = 192.168.0.0/16 172.16.0.0/12 10.0.0.0/8 | subnets = 192.168.0.0/16 172.16.0.0/12 10.0.0.0/8 | ||||||
| @@ -16,37 +19,96 @@ subnets = 192.168.0.0/16 172.16.0.0/12 10.0.0.0/8 | |||||||
| ike=aes256-sha384-{{ dhgroup }}! | ike=aes256-sha384-{{ dhgroup }}! | ||||||
| esp=aes128gcm16-aes128gmac-{{ dhgroup }}! | esp=aes128gcm16-aes128gmac-{{ dhgroup }}! | ||||||
|  |  | ||||||
| [tpl-archer-c7] |  | ||||||
| # Title shown in the UI |  | ||||||
| title = TP-Link Archer C7 (Access Point) |  | ||||||
|  |  | ||||||
| # Script to build the image, copy file to /etc/certidude/ and make modifications as necessary | [tpl-wdr3600-factory] | ||||||
| command = {{ doc_path }}/builder/ap.sh | enabled = no | ||||||
|  |  | ||||||
|  | # Title shown in the UI | ||||||
|  | title = TP-Link WDR3600 (Access Point), TFTP-friendly | ||||||
|  |  | ||||||
|  | # Script to build the image, copy file to /etc/certidude/script/ and make modifications as necessary | ||||||
|  | command = /srv/certidude/doc/builder/ap.sh | ||||||
|  |  | ||||||
| # Device/model/profile selection | # Device/model/profile selection | ||||||
| model = archer-c7-v2 | model = tl-wdr3600-v1 | ||||||
|  |  | ||||||
| # File that will be picked from the bin/ folder | # File that will be picked from the bin/ folder | ||||||
| filename = archer-c7-v2-squashfs-factory-eu.bin | filename = tl-wdr3600-v1-squashfs-factory.bin | ||||||
|  |  | ||||||
| # And renamed to make it TFTP-friendly | # And renamed to make it TFTP-friendly | ||||||
|  | rename = wdr4300v1_tp_recovery.bin | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | [tpl-wdr4300-factory] | ||||||
|  | enabled = no | ||||||
|  | title = TP-Link WDR4300 (Access Point), TFTP-friendly | ||||||
|  | command = /srv/certidude/doc/builder/ap.sh | ||||||
|  | model = tl-wdr4300-v1 | ||||||
|  | filename = tl-wdr4300-v1-squashfs-factory.bin | ||||||
|  | rename = wdr4300v1_tp_recovery.bin | ||||||
|  |  | ||||||
|  | [tpl-archer-c7-factory] | ||||||
|  | enabled = no | ||||||
|  | title = TP-Link Archer C7 (Access Point), TFTP-friendly | ||||||
|  | command = {{ doc_path }}/builder/ap.sh | ||||||
|  | model = archer-c7-v2 | ||||||
|  | filename = archer-c7-v2-squashfs-factory-eu.bin | ||||||
| rename = ArcherC7v2_tp_recovery.bin | rename = ArcherC7v2_tp_recovery.bin | ||||||
|  |  | ||||||
| [cf-e380ac] | [cf-e380ac-factory] | ||||||
| title = Comfast E380AC (Access Point) | enabled = no | ||||||
|  | title = Comfast E380AC (Access Point), TFTP-friendly | ||||||
| command = {{ doc_path }}/builder/ap.sh | command = {{ doc_path }}/builder/ap.sh | ||||||
| model = cf-e380ac-v2 | model = cf-e380ac-v2 | ||||||
| filename = cf-e380ac-v2-squashfs-factory.bin | filename = cf-e380ac-v2-squashfs-factory.bin | ||||||
| rename = firmware_auto.bin | rename = firmware_auto.bin | ||||||
|  |  | ||||||
| [ar150-mfp] |  | ||||||
|  |  | ||||||
|  | [tpl-wdr3600-sysupgrade] | ||||||
|  | ;enabled = yes | ||||||
|  | title = TP-Link WDR3600 (Access Point) | ||||||
|  | command = /srv/certidude/doc/builder/ap.sh | ||||||
|  | model = tl-wdr3600-v1 | ||||||
|  | filename = tl-wdr3600-v1-squashfs-sysupgrade.bin | ||||||
|  | rename = ap-tl-wdr3600-v1-squashfs-sysupgrade.bin | ||||||
|  |  | ||||||
|  |  | ||||||
|  | [tpl-wdr4300-sysupgrade] | ||||||
|  | ;enabled = yes | ||||||
|  | title = TP-Link WDR4300 (Access Point) | ||||||
|  | command = /srv/certidude/doc/builder/ap.sh | ||||||
|  | model = tl-wdr4300-v1 | ||||||
|  | filename = tl-wdr4300-v1-squashfs-sysupgrade.bin | ||||||
|  | rename = ap-tl-wdr4300-v1-squashfs-sysupgrade.bin | ||||||
|  |  | ||||||
|  | [tpl-archer-c7-sysupgrade] | ||||||
|  | ;enabled = yes | ||||||
|  | title = TP-Link Archer C7 (Access Point) | ||||||
|  | command = {{ doc_path }}/builder/ap.sh | ||||||
|  | model = archer-c7-v2 | ||||||
|  | filename = archer-c7-v2-squashfs-factory-eu.bin | ||||||
|  | rename = ap-archer-c7-v2-squashfs-factory-eu.bin | ||||||
|  |  | ||||||
|  | [cf-e380ac-sysupgrade] | ||||||
|  | ;enabled = yes | ||||||
|  | title = Comfast E380AC (Access Point) | ||||||
|  | command = {{ doc_path }}/builder/ap.sh | ||||||
|  | model = cf-e380ac-v2 | ||||||
|  | filename = cf-e380ac-v2-squashfs-factory.bin | ||||||
|  | rename = ap-cf-e380ac-v2-squashfs-factory.bin | ||||||
|  |  | ||||||
|  | [ar150-mfp-sysupgrade] | ||||||
|  | ;enabled = yes | ||||||
| title = GL.iNet GL-AR150 (MFP) | title = GL.iNet GL-AR150 (MFP) | ||||||
| command = {{ doc_path }}/builder/mfp.sh | command = {{ doc_path }}/builder/mfp.sh | ||||||
| model = gl-ar150 | model = gl-ar150 | ||||||
| filename = ar71xx-generic-gl-ar150-squashfs-sysupgrade.bin | filename = ar71xx-generic-gl-ar150-squashfs-sysupgrade.bin | ||||||
| rename = mfp-gl-ar150-squashfs-sysupgrade.bin | rename = mfp-gl-ar150-squashfs-sysupgrade.bin | ||||||
|  |  | ||||||
| [ar150-cam] | [ar150-cam-sysupgrade] | ||||||
|  | ;enabled = yes | ||||||
| title = GL.iNet GL-AR150 (IP Camera) | title = GL.iNet GL-AR150 (IP Camera) | ||||||
| command = {{ doc_path }}/builder/ipcam.sh | command = {{ doc_path }}/builder/ipcam.sh | ||||||
| model = gl-ar150 | model = gl-ar150 | ||||||
|   | |||||||
| @@ -1,9 +1,3 @@ | |||||||
| # To set up SSL certificates using Let's Encrypt run: |  | ||||||
| # |  | ||||||
|  |  | ||||||
| # |  | ||||||
| # Also uncomment URL rewriting and SSL configuration below |  | ||||||
|  |  | ||||||
| # Basic DoS prevention measures | # Basic DoS prevention measures | ||||||
| limit_conn addr 10; | limit_conn addr 10; | ||||||
| client_body_timeout 5s; | client_body_timeout 5s; | ||||||
| @@ -72,6 +66,9 @@ server { | |||||||
|  |  | ||||||
|     # Uncomment following to enable HTTPS |     # Uncomment following to enable HTTPS | ||||||
|     #rewrite ^/$ https://$server_name$request_uri? permanent; |     #rewrite ^/$ https://$server_name$request_uri? permanent; | ||||||
|  |  | ||||||
|  |     access_log /var/log/nginx/certidude-plaintext-access.log; | ||||||
|  |     error_log /var/log/nginx/certidude-plaintext-error.log; | ||||||
| } | } | ||||||
|  |  | ||||||
| server { | server { | ||||||
| @@ -123,13 +120,16 @@ server { | |||||||
|         nchan_subscriber longpoll; |         nchan_subscriber longpoll; | ||||||
|     } |     } | ||||||
| {% endif %} | {% endif %} | ||||||
|  |  | ||||||
|  |     access_log /var/log/nginx/certidude-frontend-access.log; | ||||||
|  |     error_log /var/log/nginx/certidude-frontend-error.log; | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| server { | server { | ||||||
|     # Section for certificate authenticated HTTPS clients, |     # Section for certificate authenticated HTTPS clients, | ||||||
|     # for submitting information to CA eg. leases, |     # for submitting information to CA eg. leases, | ||||||
|     # renewing certificates and |     # requesting/renewing certificates and | ||||||
|     # for delivering scripts to clients |     # for delivering scripts to clients | ||||||
|  |  | ||||||
|     server_name {{ common_name }}; |     server_name {{ common_name }}; | ||||||
| @@ -150,6 +150,9 @@ server { | |||||||
|         nchan_channel_id $1; |         nchan_channel_id $1; | ||||||
|         nchan_subscriber longpoll; |         nchan_subscriber longpoll; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     access_log /var/log/nginx/certidude-mutual-auth-access.log; | ||||||
|  |     error_log /var/log/nginx/certidude-mutual-auth-error.log; | ||||||
| } | } | ||||||
|  |  | ||||||
| {% if not push_server %} | {% if not push_server %} | ||||||
| @@ -167,6 +170,10 @@ server { | |||||||
|         nchan_publisher; |         nchan_publisher; | ||||||
|         nchan_channel_id $1; |         nchan_channel_id $1; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     access_log /var/log/nginx/certidude-push-access.log; | ||||||
|  |     error_log /var/log/nginx/certidude-push-error.log; | ||||||
|  |  | ||||||
| } | } | ||||||
| {% endif %} | {% endif %} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| [DEFAULT] | [DEFAULT] | ||||||
|  | enabled = no | ||||||
| ou = | ou = | ||||||
| lifetime = 120 | lifetime = 120 | ||||||
| ca = false | ca = false | ||||||
| @@ -6,7 +7,19 @@ common name = RE_COMMON_NAME | |||||||
| key usage = digital_signature key_encipherment | key usage = digital_signature key_encipherment | ||||||
| extended key usage = | extended key usage = | ||||||
|  |  | ||||||
|  | # Strongswan can automatically fetch CRL if | ||||||
|  | # CRL distribution point extension is included in the certificate | ||||||
|  | ;revoked url = | ||||||
|  | revoked url = {{ revoked_url }} | ||||||
|  |  | ||||||
|  | # StrongSwan can automatically query OCSP responder if | ||||||
|  | # AIA extension includes OCSP responder URL | ||||||
|  | ;responder url = | ||||||
|  | ;responder url = no check | ||||||
|  | responder url = {{ responder_url }} | ||||||
|  |  | ||||||
| [ca] | [ca] | ||||||
|  | enabled = yes | ||||||
| title = Certificate Authority | title = Certificate Authority | ||||||
| common name = ^ca | common name = ^ca | ||||||
| ca = true | ca = true | ||||||
| @@ -15,12 +28,14 @@ extended key usage = | |||||||
| lifetime = 1095 | lifetime = 1095 | ||||||
|  |  | ||||||
| [rw] | [rw] | ||||||
|  | enabled = yes | ||||||
| title = Roadwarrior | title = Roadwarrior | ||||||
| ou = Roadwarrior | ou = Roadwarrior | ||||||
| common name = RE_HOSTNAME | common name = RE_HOSTNAME | ||||||
| extended key usage = client_auth | extended key usage = client_auth | ||||||
|  |  | ||||||
| [srv] | [srv] | ||||||
|  | enabled = yes | ||||||
| title = Server | title = Server | ||||||
| ou = Server | ou = Server | ||||||
| common name = RE_FQDN | common name = RE_FQDN | ||||||
| @@ -28,6 +43,7 @@ lifetime = 120 | |||||||
| extended key usage = server_auth client_auth | extended key usage = server_auth client_auth | ||||||
|  |  | ||||||
| [gw] | [gw] | ||||||
|  | enabled = yes | ||||||
| title = Gateway | title = Gateway | ||||||
| ou = Gateway | ou = Gateway | ||||||
| common name = RE_FQDN | common name = RE_FQDN | ||||||
| @@ -36,6 +52,7 @@ lifetime = 120 | |||||||
| extended key usage = server_auth 1.3.6.1.5.5.8.2.2 client_auth | extended key usage = server_auth 1.3.6.1.5.5.8.2.2 client_auth | ||||||
|  |  | ||||||
| [ap] | [ap] | ||||||
|  | enabled = no | ||||||
| title = Access Point | title = Access Point | ||||||
| ou = Access Point | ou = Access Point | ||||||
| common name = RE_HOSTNAME | common name = RE_HOSTNAME | ||||||
| @@ -43,6 +60,7 @@ lifetime = 120 | |||||||
| extended key usage = client_auth | extended key usage = client_auth | ||||||
|  |  | ||||||
| [mfp] | [mfp] | ||||||
|  | enabled = no | ||||||
| title = Printers | title = Printers | ||||||
| ou = MFP | ou = MFP | ||||||
| common name = ^mfp\- | common name = ^mfp\- | ||||||
| @@ -50,9 +68,16 @@ lifetime = 120 | |||||||
| extended key usage = client_auth | extended key usage = client_auth | ||||||
|  |  | ||||||
| [cam] | [cam] | ||||||
|  | enabled = no | ||||||
| title = Camera | title = Camera | ||||||
| ou = IP Camera | ou = IP Camera | ||||||
| common name = ^cam\- | common name = ^cam\- | ||||||
| lifetime = 120 | lifetime = 120 | ||||||
| extended key usage = client_auth | extended key usage = client_auth | ||||||
|  |  | ||||||
|  | [ocsp] | ||||||
|  | enabled = no | ||||||
|  | title = OCSP Responder | ||||||
|  | common name = ^ocsp | ||||||
|  | lifetime = 7 | ||||||
|  | responder url = nocheck | ||||||
|   | |||||||
| @@ -68,6 +68,9 @@ ldap base = {{ base }} | |||||||
| ldap base = dc=example,dc=lan | ldap base = dc=example,dc=lan | ||||||
| {% endif %} | {% endif %} | ||||||
|  |  | ||||||
|  | ldap mail attribute = mail | ||||||
|  | ;ldap mail attribute = otherMailbox | ||||||
|  |  | ||||||
| [authorization] | [authorization] | ||||||
| # The authorization backend specifies how the users are authorized. | # The authorization backend specifies how the users are authorized. | ||||||
| # In case of 'posix' simply group membership is asserted, | # In case of 'posix' simply group membership is asserted, | ||||||
| @@ -182,16 +185,6 @@ revocation list lifetime = 24 | |||||||
| # URL where CA certificate can be fetched from | # URL where CA certificate can be fetched from | ||||||
| authority certificate url = {{ certificate_url }} | authority certificate url = {{ certificate_url }} | ||||||
|  |  | ||||||
| # Strongswan can automatically fetch CRL if |  | ||||||
| # CRL distribution point extension is included in the certificate |  | ||||||
| ;revoked url = |  | ||||||
| revoked url = {{ revoked_url }} |  | ||||||
|  |  | ||||||
| # StrongSwan can automatically query OCSP responder if |  | ||||||
| # AIA extension includes OCSP responder URL |  | ||||||
| responder url = |  | ||||||
| ;responder url = {{ responder_url }} |  | ||||||
|  |  | ||||||
|  |  | ||||||
| [push] | [push] | ||||||
| # This should occasionally be regenerated | # This should occasionally be regenerated | ||||||
| @@ -242,8 +235,12 @@ expired dir = {{ directory }}/expired/ | |||||||
| # and make sure Certidude machine doesn't try to accept mails. | # and make sure Certidude machine doesn't try to accept mails. | ||||||
| # uncomment mail sender address to enable e-mails. | # uncomment mail sender address to enable e-mails. | ||||||
| # Make sure used e-mail address is reachable for end users. | # Make sure used e-mail address is reachable for end users. | ||||||
| name = Certificate management | name = Certidude at {{ common_name }} | ||||||
| address = certificates@example.lan | {% if domain %} | ||||||
|  | address = certificates@{{ domain }} | ||||||
|  | {% else %} | ||||||
|  | address = certificates@exaple.com | ||||||
|  | {% endif %} | ||||||
|  |  | ||||||
| [tagging] | [tagging] | ||||||
| owner/string = Owner | owner/string = Owner | ||||||
| @@ -259,17 +256,20 @@ services template = {{ template_path }}/bootstrap.conf | |||||||
|  |  | ||||||
| [token] | [token] | ||||||
| # Token mechanism allows authority administrator to send invites for users. | # Token mechanism allows authority administrator to send invites for users. | ||||||
| # Token API call /api/token/ could be for example exposed on the internet via proxypass. | # Backend for tokens, set none to disable | ||||||
| # Token mechanism disabled by setting URL setting to none | ;backend = | ||||||
| ;url = http://ca.example.com/ | backend = sql | ||||||
| url = |  | ||||||
|  |  | ||||||
| # Token lifetime in minutes, 30 minutes by default. | # Database path for SQL backend | ||||||
|  | database = sqlite://{{ directory }}/meta/db.sqlite | ||||||
|  |  | ||||||
|  | # URL format | ||||||
|  | url = {{ token_url }} | ||||||
|  |  | ||||||
|  | # Token lifetime in minutes, 48 hours by default. | ||||||
| # Note that code tolerates 5 minute clock skew. | # Note that code tolerates 5 minute clock skew. | ||||||
| lifetime = 30 | lifetime = 2880 | ||||||
|  |  | ||||||
| # Secret for generating and validating tokens, regenerate occasionally |  | ||||||
| secret = {{ token_secret }} |  | ||||||
|  |  | ||||||
| [script] | [script] | ||||||
| # Path to the folder with scripts that can be served to the clients, set none to disable scripting | # Path to the folder with scripts that can be served to the clients, set none to disable scripting | ||||||
| @@ -279,4 +279,4 @@ path = {{ script_dir }} | |||||||
|  |  | ||||||
| [service] | [service] | ||||||
| protocols = ikev2 https openvpn | protocols = ikev2 https openvpn | ||||||
| routers = ^router\d?\. | routers = ^(router|vpn|gw|gateway)\d*\. | ||||||
|   | |||||||
							
								
								
									
										34
									
								
								certidude/templates/server/site.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,34 @@ | |||||||
|  | # Configure port tagging | ||||||
|  | uci set network.lan.ifname='eth0.3' # Protected network VLAN3 tagged | ||||||
|  | uci set network.guest.ifname='eth0.4' # Public network VLAN4 tagged | ||||||
|  |  | ||||||
|  | # Configure wireless networks | ||||||
|  | for band in 2ghz 5ghz; do | ||||||
|  |     uci delete wireless.radio$band.disabled | ||||||
|  |     uci set wireless.radio$band.country=EE | ||||||
|  |  | ||||||
|  |     uci set wireless.guest$band=wifi-iface | ||||||
|  |     uci set wireless.guest$band.network=guest | ||||||
|  |     uci set wireless.guest$band.mode=ap | ||||||
|  |     uci set wireless.guest$band.device=radio$band | ||||||
|  |     uci set wireless.guest$band.encryption=none | ||||||
|  |     uci set wireless.guest$band.ssid="k-space.ee guest" | ||||||
|  |  | ||||||
|  |     uci set wireless.lan$band=wifi-iface | ||||||
|  |     uci set wireless.lan$band.network=lan | ||||||
|  |     uci set wireless.lan$band.mode=ap | ||||||
|  |     uci set wireless.lan$band.device=radio$band | ||||||
|  |     uci set wireless.lan$band.encryption=psk2+ccmp | ||||||
|  |     uci set wireless.lan$band.ssid="k-space protected" | ||||||
|  |     uci set wireless.lan$band.key="salakala" | ||||||
|  |  | ||||||
|  | done | ||||||
|  |  | ||||||
|  | # Add Lauri's Yubikey | ||||||
|  | cat > /etc/dropbear/authorized_keys << \EOF | ||||||
|  | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCb4iqSrJrA13ygAZTZb6ElPsMXrlXXrztxt3bcKuEbAiWOm9lR17puRLMZbM2tvAW+iwsDHfQAs0E6HDprP68nt+SGkQvItUtYeJBWDI405DbRodmDMySahmb6o6S3sqI4vryydOg1G+Z0DITksZzp91Ow+C++emk6aqWfXh7xATexCvKphfwXrBL+MDIwx6drIiN0FD08yd/zxGAlcQpR8o6uecmXdk32wL5W3+qqwbJrLjZmOweij5KSXuEARuQhM20KXzYzzQIAKqhIoALRSEX31L0bwxOqfVaotzk4TWKJSeetEhBOd7PtH0ZrmOHF+B20Ym+V3UkRY5P4calF | ||||||
|  | EOF | ||||||
|  |  | ||||||
|  | # Set root password to 'salakala' | ||||||
|  | sed -i 's|^root::|root:$1$S0wGaZqK$fzEzb0WTC5.WHm2Fz9UI9.:|' /etc/shadow | ||||||
|  |  | ||||||
							
								
								
									
										85
									
								
								certidude/tokens.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,85 @@ | |||||||
|  |  | ||||||
|  | import string | ||||||
|  | from datetime import datetime, timedelta | ||||||
|  | from certidude import authority, config, mailer, const | ||||||
|  | from certidude.relational import RelationalMixin | ||||||
|  | from certidude.common import random | ||||||
|  |  | ||||||
|  | class TokenManager(RelationalMixin): | ||||||
|  |     SQL_CREATE_TABLES = "token_tables.sql" | ||||||
|  |  | ||||||
|  |     def consume(self, uuid): | ||||||
|  |         now = datetime.utcnow() | ||||||
|  |         retval = self.get( | ||||||
|  |             "select subject, mail, created, expires, profile from token where uuid = ? and created < ? and ? < expires and used is null", | ||||||
|  |             uuid, | ||||||
|  |             now + const.CLOCK_SKEW_TOLERANCE, | ||||||
|  |             now - const.CLOCK_SKEW_TOLERANCE) | ||||||
|  |         self.execute( | ||||||
|  |             "update token set used = ? where uuid = ?", | ||||||
|  |             now, | ||||||
|  |             uuid) | ||||||
|  |         return retval | ||||||
|  |  | ||||||
|  |     def issue(self, issuer, subject, subject_mail=None): | ||||||
|  |         # Expand variables | ||||||
|  |         subject_username = subject.name | ||||||
|  |         if not subject_mail: | ||||||
|  |             subject_mail = subject.mail | ||||||
|  |  | ||||||
|  |         # Generate token | ||||||
|  |         token = ''.join(random.choice(string.ascii_lowercase + string.ascii_uppercase + string.digits) for _ in range(32)) | ||||||
|  |         token_created = datetime.utcnow() | ||||||
|  |         token_expires = token_created + config.TOKEN_LIFETIME | ||||||
|  |  | ||||||
|  |         self.sql_execute("token_issue.sql", | ||||||
|  |             token_created, token_expires, token, | ||||||
|  |             issuer.name if issuer else None, | ||||||
|  |             subject_username, subject_mail, "rw") | ||||||
|  |  | ||||||
|  |         # Token lifetime in local time, to select timezone: dpkg-reconfigure tzdata | ||||||
|  |         try: | ||||||
|  |             with open("/etc/timezone") as fh: | ||||||
|  |                 token_timezone = fh.read().strip() | ||||||
|  |         except EnvironmentError: | ||||||
|  |             token_timezone = None | ||||||
|  |  | ||||||
|  |         router = sorted([j[0] for j in authority.list_signed( | ||||||
|  |                     common_name=config.SERVICE_ROUTERS)])[0] | ||||||
|  |         protocols = ",".join(config.SERVICE_PROTOCOLS) | ||||||
|  |         url = config.TOKEN_URL % locals() | ||||||
|  |  | ||||||
|  |         context = globals() | ||||||
|  |         context.update(locals()) | ||||||
|  |  | ||||||
|  |         mailer.send("token.md", to=subject_mail, **context) | ||||||
|  |         return { | ||||||
|  |             "token": token, | ||||||
|  |             "url": url, | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |     def list(self, expired=False, used=False, token=False): | ||||||
|  |         stmt = "select created as 'created[timestamp]', expires as 'expires[timestamp]', used as 'used[timestamp]', issuer, mail, subject" | ||||||
|  |         if token: | ||||||
|  |             stmt += ", uuid" | ||||||
|  |         stmt += " from token" | ||||||
|  |         where = [] | ||||||
|  |         args = [] | ||||||
|  |         if not expired: | ||||||
|  |             where.append(" expires > ?") | ||||||
|  |             args.append(datetime.utcnow()) | ||||||
|  |         if not used: | ||||||
|  |             where.append(" used is null") | ||||||
|  |         if where: | ||||||
|  |             stmt = stmt + " where " + (" and ".join(where)) | ||||||
|  |         stmt += " order by expires" | ||||||
|  |  | ||||||
|  |         return self.iterfetch(stmt, *args) | ||||||
|  |  | ||||||
|  |     def purge(self, all=False): | ||||||
|  |         stmt = "delete from token" | ||||||
|  |         args = [] | ||||||
|  |         if not all: | ||||||
|  |             stmt += " where expires < ?" | ||||||
|  |             args.append(datetime.utcnow()) | ||||||
|  |         return self.execute(stmt, *args) | ||||||
| @@ -22,6 +22,8 @@ class User(object): | |||||||
|         return hash(self.mail) |         return hash(self.mail) | ||||||
|  |  | ||||||
|     def __eq__(self, other): |     def __eq__(self, other): | ||||||
|  |         if other == None: | ||||||
|  |             return False | ||||||
|         assert isinstance(other, User), "%s is not instance of User" % repr(other) |         assert isinstance(other, User), "%s is not instance of User" % repr(other) | ||||||
|         return self.mail == other.mail |         return self.mail == other.mail | ||||||
|  |  | ||||||
| @@ -90,7 +92,7 @@ class ActiveDirectoryUserManager(object): | |||||||
|         # TODO: Sanitize username |         # TODO: Sanitize username | ||||||
|         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", config.LDAP_MAIL_ATTRIBUTE, "userPrincipalName" | ||||||
|             r = conn.search_s(config.LDAP_BASE, 2, ft, attribs) |             r = conn.search_s(config.LDAP_BASE, 2, ft, attribs) | ||||||
|             for dn, entry in r: |             for dn, entry in r: | ||||||
|                 if not dn: |                 if not dn: | ||||||
| @@ -105,21 +107,21 @@ class ActiveDirectoryUserManager(object): | |||||||
|                     else: |                     else: | ||||||
|                         given_name, surname = cn, b"" |                         given_name, surname = cn, b"" | ||||||
|  |  | ||||||
|                 mail, = entry.get("mail") or entry.get("userPrincipalName") or ((username + "@" + const.DOMAIN).encode("ascii"),) |                 mail, = entry.get(config.LDAP_MAIL_ATTRIBUTE) or ((username + "@" + const.DOMAIN).encode("ascii"),) | ||||||
|                 return User(username, mail.decode("ascii"), |                 return User(username, mail.decode("ascii"), | ||||||
|                     given_name.decode("utf-8"), surname.decode("utf-8")) |                     given_name.decode("utf-8"), surname.decode("utf-8")) | ||||||
|             raise User.DoesNotExist("User %s does not exist" % username) |             raise User.DoesNotExist("User %s does not exist" % username) | ||||||
|  |  | ||||||
|     def filter(self, ft): |     def filter(self, ft): | ||||||
|         with DirectoryConnection() as conn: |         with DirectoryConnection() as conn: | ||||||
|             attribs = "givenName", "surname", "samaccountname", "cn", "mail", "userPrincipalName" |             attribs = "givenName", "surname", "samaccountname", "cn", config.LDAP_MAIL_ATTRIBUTE, "userPrincipalName" | ||||||
|             r = conn.search_s(config.LDAP_BASE, 2, ft, attribs) |             r = conn.search_s(config.LDAP_BASE, 2, ft, attribs) | ||||||
|             for dn,entry in r: |             for dn,entry in r: | ||||||
|                 if not dn: |                 if not dn: | ||||||
|                     continue |                     continue | ||||||
|                 username, = entry.get("sAMAccountName") |                 username, = entry.get("sAMAccountName") | ||||||
|                 cn, = entry.get("cn") |                 cn, = entry.get("cn") | ||||||
|                 mail, = entry.get("mail") or entry.get("userPrincipalName") or (username + b"@" + const.DOMAIN.encode("ascii"),) |                 mail, = entry.get(config.LDAP_MAIL_ATTRIBUTE) or entry.get("userPrincipalName") or (username + b"@" + const.DOMAIN.encode("ascii"),) | ||||||
|                 if entry.get("givenName") and entry.get("sn"): |                 if entry.get("givenName") and entry.get("sn"): | ||||||
|                     given_name, = entry.get("givenName") |                     given_name, = entry.get("givenName") | ||||||
|                     surname, = entry.get("sn") |                     surname, = entry.get("sn") | ||||||
|   | |||||||
| @@ -111,7 +111,7 @@ EOF | |||||||
| make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="luci \ | make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="luci \ | ||||||
|     openssl-util curl ca-certificates dropbear \ |     openssl-util curl ca-certificates dropbear \ | ||||||
|     strongswan-mod-kernel-libipsec kmod-tun strongswan-default strongswan-mod-openssl strongswan-mod-curl strongswan-mod-ccm strongswan-mod-gcm \ |     strongswan-mod-kernel-libipsec kmod-tun strongswan-default strongswan-mod-openssl strongswan-mod-curl strongswan-mod-ccm strongswan-mod-gcm \ | ||||||
|     htop iftop tcpdump nmap nano -odhcp6c -odhcpd -dnsmasq \ |     htop iftop netdata -odhcp6c -odhcpd -dnsmasq \ | ||||||
|     -luci-app-firewall \ |     -luci-app-firewall \ | ||||||
|     -pppd -luci-proto-ppp -kmod-ppp -ppp -ppp-mod-pppoe \ |     -pppd -luci-proto-ppp -kmod-ppp -ppp -ppp-mod-pppoe \ | ||||||
|     -kmod-ip6tables -ip6tables -luci-proto-ipv6 -kmod-iptunnel6 -kmod-ipsec6" |     -kmod-ip6tables -ip6tables -luci-proto-ipv6 -kmod-iptunnel6 -kmod-ipsec6" | ||||||
|   | |||||||
| @@ -29,9 +29,7 @@ AUTHORITY=$(hostname -f) | |||||||
| mkdir -p $OVERLAY/etc/config | mkdir -p $OVERLAY/etc/config | ||||||
| mkdir -p $OVERLAY/etc/uci-defaults | mkdir -p $OVERLAY/etc/uci-defaults | ||||||
| mkdir -p $OVERLAY/etc/certidude/authority/$AUTHORITY/ | mkdir -p $OVERLAY/etc/certidude/authority/$AUTHORITY/ | ||||||
| cp /var/lib/certidude/$AUTHORITY/ca_cert.pem $OVERLAY/etc/certidude/authority/$AUTHORITY/ | cp /var/lib/certidude/ca_cert.pem $OVERLAY/etc/certidude/authority/$AUTHORITY/ | ||||||
|  |  | ||||||
| echo /etc/certidude >> $OVERLAY/etc/sysupgrade.conf |  | ||||||
|  |  | ||||||
| cat <<EOF > $OVERLAY/etc/config/certidude | cat <<EOF > $OVERLAY/etc/config/certidude | ||||||
|  |  | ||||||
|   | |||||||
| @@ -40,5 +40,7 @@ EOF | |||||||
|  |  | ||||||
| make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="openssl-util curl ca-certificates \ | make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="openssl-util curl ca-certificates \ | ||||||
|     strongswan-default strongswan-mod-openssl strongswan-mod-curl strongswan-mod-ccm strongswan-mod-gcm htop \ |     strongswan-default strongswan-mod-openssl strongswan-mod-curl strongswan-mod-ccm strongswan-mod-gcm htop \ | ||||||
|     iftop tcpdump nmap nano mtr patch diffutils ipset usbutils luci luci-app-mjpg-streamer kmod-video-uvc dropbear \ |     iftop tcpdump nmap nano usbutils luci luci-app-mjpg-streamer kmod-video-uvc dropbear \ | ||||||
|     pciutils -dnsmasq -odhcpd -odhcp6c -kmod-ath9k picocom strongswan-mod-kernel-libipsec kmod-tun" |     -pppd -luci-proto-ppp -kmod-ppp -ppp -ppp-mod-pppoe \ | ||||||
|  |     -dnsmasq -odhcpd -odhcp6c -kmod-ath9k picocom strongswan-mod-kernel-libipsec kmod-tun \ | ||||||
|  |     netdata" | ||||||
|   | |||||||
| @@ -103,8 +103,9 @@ uci set uhttpd.main.listen_http=0.0.0.0:8080 | |||||||
| EOF | EOF | ||||||
|  |  | ||||||
| make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="openssl-util curl ca-certificates htop \ | make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="openssl-util curl ca-certificates htop \ | ||||||
|     iftop tcpdump nmap nano mtr patch diffutils ipset usbutils luci dropbear kmod-tun \ |     iftop tcpdump nmap nano mtr patch diffutils ipset usbutils luci dropbear kmod-tun netdata \ | ||||||
|     strongswan-default strongswan-mod-kernel-libipsec strongswan-mod-openssl strongswan-mod-curl strongswan-mod-ccm strongswan-mod-gcm \ |     strongswan-default strongswan-mod-kernel-libipsec strongswan-mod-openssl strongswan-mod-curl strongswan-mod-ccm strongswan-mod-gcm \ | ||||||
|     pciutils -odhcpd -odhcp6c -kmod-ath9k picocom libustream-openssl kmod-crypto-gcm bc" |     -odhcpd -odhcp6c -kmod-ath9k picocom libustream-openssl kmod-crypto-gcm \ | ||||||
|  |     -pppd -luci-proto-ppp -kmod-ppp -ppp -ppp-mod-pppoe \ | ||||||
|  |     -kmod-ip6tables -ip6tables -luci-proto-ipv6 -kmod-iptunnel6 -kmod-ipsec6" | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,11 @@ | |||||||
|  | # Randomize restart time | ||||||
|  | OFFSET=$(awk -v min=1 -v max=59 'BEGIN{srand(); print int(min+rand()*(max-min+1))}') | ||||||
|  |  | ||||||
|  | # wtf?! https://wiki.strongswan.org/issues/1501#note-7 | ||||||
| cat << EOF > /etc/crontabs/root | cat << EOF > /etc/crontabs/root | ||||||
| 15 1 * * * sleep 70 && touch /etc/banner && reboot | #$OFFSET 2 * * * sleep 70 && touch /etc/banner && reboot | ||||||
| 10 1 1 */2 * /usr/bin/certidude-enroll-renew | $OFFSET 2 * * * ipsec restart | ||||||
|  | 5 1 1 */2 * /usr/bin/certidude-enroll-renew | ||||||
| EOF | EOF | ||||||
|  |  | ||||||
| chmod 0600 /etc/crontabs/root | chmod 0600 /etc/crontabs/root | ||||||
|   | |||||||
| @@ -122,6 +122,11 @@ logger -t certidude -s "Certificate md5sum: $(md5sum -b $CERTIFICATE_PATH.part)" | |||||||
|  |  | ||||||
| uci commit | uci commit | ||||||
|  |  | ||||||
|  | echo $AUTHORITY_PATH >> /etc/sysupgrade.conf | ||||||
|  | echo $CERTIFICATE_PATH >> /etc/sysupgrade.conf | ||||||
|  | echo $KEY_PATH >> /etc/sysupgrade.conf | ||||||
|  | echo $REQUEST_PATH >> /etc/sysupgrade.conf | ||||||
|  |  | ||||||
| mv $CERTIFICATE_PATH.part $CERTIFICATE_PATH | mv $CERTIFICATE_PATH.part $CERTIFICATE_PATH | ||||||
|  |  | ||||||
| # Start services | # Start services | ||||||
|   | |||||||
| @@ -8,6 +8,8 @@ CERTIFICATE_PATH=$DIR/host_cert.pem | |||||||
| REQUEST_PATH=$DIR/host_req.pem | REQUEST_PATH=$DIR/host_req.pem | ||||||
| KEY_PATH=$DIR/host_key.pem | KEY_PATH=$DIR/host_key.pem | ||||||
|  |  | ||||||
|  | # TODO: fix Accepted 202 here | ||||||
|  |  | ||||||
| curl -f -L \ | curl -f -L \ | ||||||
|     -H "Content-Type: application/pkcs10" \ |     -H "Content-Type: application/pkcs10" \ | ||||||
|     --data-binary @$REQUEST_PATH \ |     --data-binary @$REQUEST_PATH \ | ||||||
|   | |||||||
| @@ -1,6 +1,8 @@ | |||||||
| import pwd | import pwd | ||||||
|  | from asn1crypto import pem, x509 | ||||||
| from oscrypto import asymmetric | from oscrypto import asymmetric | ||||||
| from csrbuilder import CSRBuilder, pem_armor_csr | from csrbuilder import CSRBuilder, pem_armor_csr | ||||||
|  | from asn1crypto.util import OrderedDict | ||||||
| from subprocess import check_output | from subprocess import check_output | ||||||
| from importlib import reload | from importlib import reload | ||||||
| from click.testing import CliRunner | from click.testing import CliRunner | ||||||
| @@ -86,6 +88,9 @@ def clean_client(): | |||||||
|  |  | ||||||
|  |  | ||||||
| def clean_server(): | def clean_server(): | ||||||
|  |     # Stop Samba | ||||||
|  |     os.system("systemctl stop samba-ad-dc") | ||||||
|  |  | ||||||
|     os.umask(0o22) |     os.umask(0o22) | ||||||
|  |  | ||||||
|     if os.path.exists("/run/certidude/server.pid"): |     if os.path.exists("/run/certidude/server.pid"): | ||||||
| @@ -134,14 +139,9 @@ def clean_server(): | |||||||
|         if os.path.exists("/etc/openvpn/keys"): |         if os.path.exists("/etc/openvpn/keys"): | ||||||
|             shutil.rmtree("/etc/openvpn/keys") |             shutil.rmtree("/etc/openvpn/keys") | ||||||
|  |  | ||||||
|     # Stop samba |     # Remove Samba stuff | ||||||
|     if os.path.exists("/run/samba/samba.pid"): |  | ||||||
|         with open("/run/samba/samba.pid") as fh: |  | ||||||
|             try: |  | ||||||
|                 os.kill(int(fh.read()), 15) |  | ||||||
|             except OSError: |  | ||||||
|                 pass |  | ||||||
|     os.system("rm -Rfv /var/lib/samba/*") |     os.system("rm -Rfv /var/lib/samba/*") | ||||||
|  |     assert not os.path.exists("/var/lib/samba/private/secrets.keytab") | ||||||
|  |  | ||||||
|     # Restore initial resolv.conf |     # Restore initial resolv.conf | ||||||
|     shutil.copyfile("/etc/resolv.conf.orig", "/etc/resolv.conf") |     shutil.copyfile("/etc/resolv.conf.orig", "/etc/resolv.conf") | ||||||
| @@ -156,7 +156,7 @@ def test_cli_setup_authority(): | |||||||
|     assert os.getuid() == 0, "Run tests as root in a clean VM or container" |     assert os.getuid() == 0, "Run tests as root in a clean VM or container" | ||||||
|     assert check_output(["/bin/hostname", "-f"]) == b"ca.example.lan\n", "As a safety precaution, unittests only run in a machine whose hostanme -f  is ca.example.lan" |     assert check_output(["/bin/hostname", "-f"]) == b"ca.example.lan\n", "As a safety precaution, unittests only run in a machine whose hostanme -f  is ca.example.lan" | ||||||
|  |  | ||||||
|     os.system("DEBIAN_FRONTEND=noninteractive apt-get install -qq -y git build-essential python-dev libkrb5-dev samba krb5-user winbind bc") |     os.system("DEBIAN_FRONTEND=noninteractive apt-get install -qq -y git build-essential python-dev libkrb5-dev samba krb5-user") | ||||||
|  |  | ||||||
|     assert_cleanliness() |     assert_cleanliness() | ||||||
|  |  | ||||||
| @@ -211,10 +211,19 @@ def test_cli_setup_authority(): | |||||||
|     assert const.HOSTNAME == "ca" |     assert const.HOSTNAME == "ca" | ||||||
|     assert const.DOMAIN == "example.lan" |     assert const.DOMAIN == "example.lan" | ||||||
|  |  | ||||||
|  |     # Bootstrap authority again with: | ||||||
|  |     # - ECDSA certificates | ||||||
|  |     # - POSIX auth | ||||||
|  |     # - OCSP enabled | ||||||
|  |     # - SCEP disabled | ||||||
|  |     # - CRL enabled | ||||||
|  |  | ||||||
|     assert os.system("certidude setup authority --elliptic-curve") == 0 |     assert os.system("certidude setup authority --elliptic-curve") == 0 | ||||||
|  |  | ||||||
|     assert_cleanliness() |     assert_cleanliness() | ||||||
|  |  | ||||||
|  |     assert os.path.exists("/var/lib/certidude/signed/ca.example.lan.pem"), "provisioning failed" | ||||||
|  |     assert not os.path.exists("/etc/cron.hourly/certidude") | ||||||
|  |  | ||||||
|     # Make sure nginx is running |     # Make sure nginx is running | ||||||
|     assert os.system("nginx -t") == 0, "invalid nginx configuration" |     assert os.system("nginx -t") == 0, "invalid nginx configuration" | ||||||
| @@ -222,12 +231,6 @@ def test_cli_setup_authority(): | |||||||
|  |  | ||||||
|     # Make sure we generated legit CA certificate |     # Make sure we generated legit CA certificate | ||||||
|     from certidude import config, authority, user |     from certidude import config, authority, user | ||||||
|     assert authority.certificate.serial_number >= 0x100000000000000000000000000000000000000 |  | ||||||
|     assert authority.certificate.serial_number <= 0xfffffffffffffffffffffffffffffffffffffff |  | ||||||
|     assert authority.certificate["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None) < datetime.utcnow() |  | ||||||
|     assert authority.certificate["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None) > datetime.utcnow() + timedelta(days=7000) |  | ||||||
|     assert authority.certificate["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None) < datetime.utcnow() |  | ||||||
|     assert authority.public_key.algorithm == "ec" |  | ||||||
|  |  | ||||||
|     # Generate garbage |     # Generate garbage | ||||||
|     with open("/var/lib/certidude/bla", "w") as fh: |     with open("/var/lib/certidude/bla", "w") as fh: | ||||||
| @@ -240,7 +243,6 @@ def test_cli_setup_authority(): | |||||||
|         pass |         pass | ||||||
|  |  | ||||||
|     # Start server before any signing operations are performed |     # Start server before any signing operations are performed | ||||||
|     config.CERTIFICATE_RENEWAL_ALLOWED = True |  | ||||||
|     assert_cleanliness() |     assert_cleanliness() | ||||||
|  |  | ||||||
|     import requests |     import requests | ||||||
| @@ -255,16 +257,40 @@ def test_cli_setup_authority(): | |||||||
|  |  | ||||||
|  |  | ||||||
|     # Test CA certificate fetch |     # Test CA certificate fetch | ||||||
|     buf = open("/var/lib/certidude/ca_cert.pem").read() |  | ||||||
|     r = requests.get("http://ca.example.lan/api/certificate") |     r = requests.get("http://ca.example.lan/api/certificate") | ||||||
|     assert r.status_code == 200 |     assert r.status_code == 200 | ||||||
|     assert r.headers.get('content-type') == "application/x-x509-ca-cert" |     assert r.headers.get('content-type') == "application/x-x509-ca-cert" | ||||||
|     assert r.text == buf |     header, _, certificate_der_bytes = pem.unarmor(r.text.encode("ascii")) | ||||||
|  |     cert = x509.Certificate.load(certificate_der_bytes) | ||||||
|  |  | ||||||
|  |     assert cert.subject.native.get("common_name") == "Certidude at ca.example.lan" | ||||||
|  |     assert cert.subject.native.get("organizational_unit_name") == "Certificate Authority" | ||||||
|  |     assert cert.serial_number >= 0x150000000000000000000000000000 | ||||||
|  |     assert cert.serial_number <= 0xfffffffffffffffffffffffffffffffffffffff | ||||||
|  |     assert cert["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None) < datetime.utcnow() | ||||||
|  |     assert cert["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None) > datetime.utcnow() + timedelta(days=7000) | ||||||
|  |     assert cert["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None) < datetime.utcnow() | ||||||
|  |  | ||||||
|  |     extensions = cert["tbs_certificate"]["extensions"].native | ||||||
|  |     assert extensions[0] == OrderedDict([ | ||||||
|  |         ('extn_id', 'basic_constraints'), | ||||||
|  |         ('critical', True), | ||||||
|  |         ('extn_value', OrderedDict([ | ||||||
|  |             ('ca', True), | ||||||
|  |             ('path_len_constraint', None)] | ||||||
|  |         ))]), extensions[0] | ||||||
|  | #    assert extensions[1][0] == "key_identifier", extensions[1] | ||||||
|  |  | ||||||
|  |     assert extensions[2] == OrderedDict([ | ||||||
|  |         ('extn_id', 'key_usage'), | ||||||
|  |         ('critical', True), | ||||||
|  |         ('extn_value', {'key_cert_sign', 'crl_sign'})]), extensions[3] | ||||||
|  |     assert len(extensions) == 3 | ||||||
|  |  | ||||||
|  |     public_key = asymmetric.load_public_key(cert["tbs_certificate"]["subject_public_key_info"]) | ||||||
|  |     assert public_key.algorithm == "ec" | ||||||
|  |  | ||||||
|  |  | ||||||
|     r = client().simulate_get("/api/certificate") |  | ||||||
|     assert r.status_code == 200 |  | ||||||
|     assert r.headers.get('content-type') == "application/x-x509-ca-cert" |  | ||||||
|     assert r.text == buf |  | ||||||
|  |  | ||||||
|     # Password is bot, users created by Travis |     # Password is bot, users created by Travis | ||||||
|     usertoken = "Basic dXNlcmJvdDpib3Q=" |     usertoken = "Basic dXNlcmJvdDpib3Q=" | ||||||
| @@ -419,6 +445,38 @@ def test_cli_setup_authority(): | |||||||
|     assert r.status_code == 200, r.text |     assert r.status_code == 200, r.text | ||||||
|     assert r.headers.get('content-type') == "application/x-pem-file" |     assert r.headers.get('content-type') == "application/x-pem-file" | ||||||
|  |  | ||||||
|  |     header, _, certificate_der_bytes = pem.unarmor(r.text.encode("ascii")) | ||||||
|  |     cert = x509.Certificate.load(certificate_der_bytes) | ||||||
|  |     assert cert.subject.native.get("common_name") == "test" | ||||||
|  |     assert cert.subject.native.get("organizational_unit_name") == "Roadwarrior" | ||||||
|  |     assert cert.serial_number >= 0x150000000000000000000000000000 | ||||||
|  |     assert cert.serial_number <= 0xfffffffffffffffffffffffffffffffffffffff | ||||||
|  |     assert cert["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None) < datetime.utcnow() | ||||||
|  |     assert cert["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None) > datetime.utcnow() + timedelta(days=100) | ||||||
|  |     assert cert["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None) < datetime.utcnow() | ||||||
|  |  | ||||||
|  |     public_key = asymmetric.load_public_key(cert["tbs_certificate"]["subject_public_key_info"]) | ||||||
|  |     assert public_key.algorithm == "ec" | ||||||
|  |     """ | ||||||
|  |     extensions = cert["tbs_certificate"]["extensions"].native | ||||||
|  |     assert extensions[0] == OrderedDict([ | ||||||
|  |         ('extn_id', 'basic_constraints'), | ||||||
|  |         ('critical', True), | ||||||
|  |         ('extn_value', OrderedDict([ | ||||||
|  |             ('ca', True), | ||||||
|  |             ('path_len_constraint', None)] | ||||||
|  |         ))]), extensions[0] | ||||||
|  | #    assert extensions[1][0] == "key_identifier", extensions[1] | ||||||
|  |  | ||||||
|  |     assert extensions[2] == OrderedDict([ | ||||||
|  |         ('extn_id', 'key_usage'), | ||||||
|  |         ('critical', True), | ||||||
|  |         ('extn_value', {'key_cert_sign', 'crl_sign'})]), extensions[3] | ||||||
|  |     assert len(extensions) == 3 | ||||||
|  |  | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |  | ||||||
|     r = client().simulate_get("/api/signed/test/", headers={"Accept":"application/json"}) |     r = client().simulate_get("/api/signed/test/", headers={"Accept":"application/json"}) | ||||||
|     assert r.status_code == 200, r.text |     assert r.status_code == 200, r.text | ||||||
|     assert r.headers.get('content-type') == "application/json" |     assert r.headers.get('content-type') == "application/json" | ||||||
| @@ -628,12 +686,7 @@ def test_cli_setup_authority(): | |||||||
|     assert ev_url.startswith("http://ca.example.lan/ev/sub/") |     assert ev_url.startswith("http://ca.example.lan/ev/sub/") | ||||||
|  |  | ||||||
|  |  | ||||||
|     ####################### |     # TODO: issue token, should fail because there are no routers | ||||||
|     ### Token mechanism ### |  | ||||||
|     ####################### |  | ||||||
|  |  | ||||||
|     # TODO |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     ############# |     ############# | ||||||
|     ### nginx ### |     ### nginx ### | ||||||
| @@ -768,6 +821,19 @@ def test_cli_setup_authority(): | |||||||
|     assert "Writing certificate to:" in result.output, result.output |     assert "Writing certificate to:" in result.output, result.output | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     ####################### | ||||||
|  |     ### Token mechanism ### | ||||||
|  |     ####################### | ||||||
|  |  | ||||||
|  |     r = client().simulate_post("/api/token/", | ||||||
|  |         body="username=userbot", | ||||||
|  |         headers={"content-type": "application/x-www-form-urlencoded", "Authorization":admintoken}) | ||||||
|  |     assert r.status_code == 200 | ||||||
|  |  | ||||||
|  |     # TODO: check consume | ||||||
|  |  | ||||||
|  |  | ||||||
|     ################################# |     ################################# | ||||||
|     ### Subscribe to event source ### |     ### Subscribe to event source ### | ||||||
|     ################################# |     ################################# | ||||||
| @@ -1055,8 +1121,9 @@ def test_cli_setup_authority(): | |||||||
|     clean_server() |     clean_server() | ||||||
|  |  | ||||||
|     # Bootstrap domain controller here, |     # Bootstrap domain controller here, | ||||||
|     # Samba startup takes some timec |     # Samba startup takes some time | ||||||
|     os.system("samba-tool domain provision --server-role=dc --domain=EXAMPLE --realm=EXAMPLE.LAN --host-name=ca") |     os.system("samba-tool domain provision --server-role=dc --domain=EXAMPLE --realm=EXAMPLE.LAN --host-name=ca") | ||||||
|  |     os.system("systemctl restart samba-ad-dc") | ||||||
|     os.system("samba-tool user add userbot S4l4k4l4 --given-name='User' --surname='Bot'") |     os.system("samba-tool user add userbot S4l4k4l4 --given-name='User' --surname='Bot'") | ||||||
|     os.system("samba-tool user add adminbot S4l4k4l4 --given-name='Admin' --surname='Bot'") |     os.system("samba-tool user add adminbot S4l4k4l4 --given-name='Admin' --surname='Bot'") | ||||||
|     os.system("samba-tool group addmembers 'Domain Admins' adminbot") |     os.system("samba-tool group addmembers 'Domain Admins' adminbot") | ||||||
| @@ -1069,7 +1136,7 @@ def test_cli_setup_authority(): | |||||||
|     with open("/etc/resolv.conf", "w") as fh: |     with open("/etc/resolv.conf", "w") as fh: | ||||||
|         fh.write("nameserver 127.0.0.1\nsearch example.lan\n") |         fh.write("nameserver 127.0.0.1\nsearch example.lan\n") | ||||||
|     # TODO: dig -t srv perhaps? |     # TODO: dig -t srv perhaps? | ||||||
|     os.system("samba") |  | ||||||
|  |  | ||||||
|     # Samba bind 636 late (probably generating keypair) |     # Samba bind 636 late (probably generating keypair) | ||||||
|     # so LDAPS connections below will fail |     # so LDAPS connections below will fail | ||||||
| @@ -1088,28 +1155,29 @@ def test_cli_setup_authority(): | |||||||
|     assert os.system("echo S4l4k4l4 | kinit administrator") == 0 |     assert os.system("echo S4l4k4l4 | kinit administrator") == 0 | ||||||
|     assert os.path.exists("/tmp/krb5cc_0") |     assert os.path.exists("/tmp/krb5cc_0") | ||||||
|  |  | ||||||
|     # Fork to not contaminate environment while creating service principal |     # Set up HTTP service principal | ||||||
|     spn_pid = os.fork() |  | ||||||
|     if not spn_pid: |  | ||||||
|     os.system("sed -e 's/CA/CA\\nkerberos method = system keytab/' -i /etc/samba/smb.conf ") |     os.system("sed -e 's/CA/CA\\nkerberos method = system keytab/' -i /etc/samba/smb.conf ") | ||||||
|         os.environ["KRB5_KTNAME"] = "FILE:/etc/certidude/server.keytab" |     assert os.system("KRB5_KTNAME=FILE:/etc/certidude/server.keytab net ads keytab add HTTP -k") == 0 | ||||||
|         assert os.system("net ads keytab add HTTP -k") == 0 |  | ||||||
|     assert os.path.exists("/etc/certidude/server.keytab") |     assert os.path.exists("/etc/certidude/server.keytab") | ||||||
|     os.system("chown root:certidude /etc/certidude/server.keytab") |     os.system("chown root:certidude /etc/certidude/server.keytab") | ||||||
|     os.system("chmod 640 /etc/certidude/server.keytab") |     os.system("chmod 640 /etc/certidude/server.keytab") | ||||||
|         return |  | ||||||
|     else: |  | ||||||
|         os.waitpid(spn_pid, 0) |  | ||||||
|  |  | ||||||
|     assert_cleanliness() |     assert_cleanliness() | ||||||
|     r = requests.get("http://ca.example.lan/api/") |     r = requests.get("http://ca.example.lan/api/") | ||||||
|     assert r.status_code == 502, r.text |     assert r.status_code == 502, r.text | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     # Bootstrap authority again with: | ||||||
|  |     # - RSA certificates | ||||||
|  |     # - Kerberos auth | ||||||
|  |     # - OCSP disabled | ||||||
|  |     # - SCEP enabled | ||||||
|  |     # - CRL disabled | ||||||
|  |  | ||||||
|     # Bootstrap authority |  | ||||||
|     assert not os.path.exists("/var/lib/certidude/ca_key.pem") |     assert not os.path.exists("/var/lib/certidude/ca_key.pem") | ||||||
|     assert os.system("certidude setup authority --skip-packages") == 0 |     assert os.system("certidude setup authority --skip-packages") == 0 | ||||||
|  |     assert os.path.exists("/var/lib/certidude/ca_key.pem") | ||||||
|  |     assert os.path.exists("/etc/cron.hourly/certidude") | ||||||
|  |  | ||||||
|  |  | ||||||
|     # Make modifications to /etc/certidude/server.conf so |     # Make modifications to /etc/certidude/server.conf so | ||||||
| @@ -1289,12 +1357,11 @@ def test_cli_setup_authority(): | |||||||
|  |  | ||||||
|     assert os.system("systemctl stop certidude") == 0 |     assert os.system("systemctl stop certidude") == 0 | ||||||
|  |  | ||||||
|     # Note: STORAGE_PATH was mangled above, hence it's /tmp not /var/lib/certidude |  | ||||||
|     assert open("/etc/apparmor.d/local/usr.lib.ipsec.charon").read() == \ |     assert open("/etc/apparmor.d/local/usr.lib.ipsec.charon").read() == \ | ||||||
|        "/etc/certidude/authority/ca.example.lan/client_key.pem r,\n" + \ |        "/etc/certidude/authority/ca.example.lan/client_key.pem r,\n" + \ | ||||||
|        "/etc/certidude/authority/ca.example.lan/ca_cert.pem r,\n" + \ |        "/etc/certidude/authority/ca.example.lan/ca_cert.pem r,\n" + \ | ||||||
|        "/etc/certidude/authority/ca.example.lan/client_cert.pem r,\n" |        "/etc/certidude/authority/ca.example.lan/client_cert.pem r,\n" | ||||||
|     assert len(inbox) == 0, inbox # Make sure all messages were checked |     # TODO: pop mails from /var/mail and check content | ||||||
|  |  | ||||||
|     os.system("service nginx stop") |     os.system("service nginx stop") | ||||||
|     os.system("service openvpn stop") |     os.system("service openvpn stop") | ||||||
|   | |||||||