├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── certidude ├── __init__.py ├── api │ ├── __init__.py │ ├── attrib.py │ ├── bootstrap.py │ ├── builder.py │ ├── lease.py │ ├── log.py │ ├── ocsp.py │ ├── request.py │ ├── revoked.py │ ├── scep.py │ ├── script.py │ ├── session.py │ ├── signed.py │ ├── tag.py │ ├── token.py │ └── utils │ │ ├── __init__.py │ │ └── firewall.py ├── authority.py ├── builder │ ├── ap.sh │ ├── common.sh │ ├── ipcam.sh │ ├── mfp.sh │ └── overlay │ │ ├── etc │ │ ├── hotplug.d │ │ │ └── iface │ │ │ │ └── 50-certidude │ │ ├── profile │ │ └── uci-defaults │ │ │ ├── 40-hostname │ │ │ ├── 60-cron │ │ │ ├── 90-certidude-sysupgrade │ │ │ └── 99-uhttpd-disable-https │ │ └── usr │ │ └── bin │ │ ├── certidude-enroll │ │ └── certidude-enroll-renew ├── cli.py ├── common.py ├── config.py ├── const.py ├── decorators.py ├── errors.py ├── mailer.py ├── mysqllog.py ├── profile.py ├── push.py ├── relational.py ├── sql │ ├── mysql │ │ ├── log_insert_entry.sql │ │ └── log_tables.sql │ └── sqlite │ │ ├── log_insert_entry.sql │ │ ├── log_tables.sql │ │ ├── token_issue.sql │ │ └── token_tables.sql ├── static │ ├── 502.json │ ├── css │ │ └── style.css │ ├── fonts │ │ ├── gentium-basic.woff2 │ │ ├── pt-sans.woff2 │ │ └── ubuntu-mono.woff2 │ ├── img │ │ ├── ubuntu-01-edit-connections.png │ │ ├── ubuntu-02-network-connections.png │ │ ├── ubuntu-03-import-saved-config.png │ │ ├── ubuntu-04-select-file.png │ │ ├── ubuntu-05-profile-imported.png │ │ ├── ubuntu-06-ipv4-settings.png │ │ ├── ubuntu-07-disable-default-route.png │ │ ├── ubuntu-08-activate-connection.png │ │ ├── windows-01-download-openvpn.png │ │ ├── windows-02-install-openvpn.png │ │ ├── windows-03-move-config-file.png │ │ ├── windows-04-connect.png │ │ └── windows-05-connected.png │ ├── index.html │ ├── js │ │ └── certidude.js │ └── robots.txt ├── templates │ ├── bootstrap.conf │ ├── client │ │ ├── certidude.service │ │ ├── certidude.timer │ │ └── openvpn-reconnect.service │ ├── mail │ │ ├── certificate-renewed.md │ │ ├── certificate-revoked.md │ │ ├── certificate-signed.md │ │ ├── expiration-notification.md │ │ ├── request-stored.md │ │ ├── test.md │ │ └── token.md │ ├── openvpn-client.conf │ ├── script │ │ ├── default.sh │ │ ├── openwrt.sh │ │ └── workstation.sh │ ├── server │ │ ├── backend.service │ │ ├── builder.conf │ │ ├── housekeeping-daily.service │ │ ├── housekeeping-daily.timer │ │ ├── ldap-kinit.service │ │ ├── ldap-kinit.timer │ │ ├── nginx.conf │ │ ├── profile.conf │ │ ├── responder.service │ │ ├── server.conf │ │ └── site.sh │ ├── snippets │ │ ├── ansible-site.yml │ │ ├── certidude-client.sh │ │ ├── gateway-updown.sh │ │ ├── ios.mobileconfig │ │ ├── networkmanager-openvpn.conf │ │ ├── networkmanager-strongswan.conf │ │ ├── nginx-https-site.conf │ │ ├── nginx-ocsp-cache.service │ │ ├── nginx-ocsp-cache.timer │ │ ├── nginx-tls.conf │ │ ├── ocsp-cache@.service │ │ ├── openvpn-client.conf │ │ ├── openvpn-client.sh │ │ ├── openwrt-openvpn.sh │ │ ├── renew.sh │ │ ├── request-client.ps1 │ │ ├── request-client.sh │ │ ├── request-common.sh │ │ ├── request-server.sh │ │ ├── setup-ocsp-caching.sh │ │ ├── store-authority.sh │ │ ├── strongswan-client.sh │ │ ├── strongswan-patching.sh │ │ ├── strongswan-server.sh │ │ ├── submit-request-wait.sh │ │ ├── update-trust.ps1 │ │ ├── update-trust.sh │ │ └── windows.ps1 │ ├── strongswan-site-to-client.conf │ └── views │ │ ├── attributes.html │ │ ├── authority.html │ │ ├── configuration.html │ │ ├── enroll.html │ │ ├── error.html │ │ ├── insecure.html │ │ ├── lease.html │ │ ├── logentry.html │ │ ├── request.html │ │ ├── revoked.html │ │ ├── signed.html │ │ ├── tags.html │ │ └── token.html ├── tokens.py └── user.py ├── doc ├── certidude.png ├── openwrt.md ├── strongswan-updown.sh ├── usecase-diagram.dia └── usecase-diagram.png ├── misc └── certidude ├── requirements.txt ├── setup.py └── tests └── test_cli.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | .goutputstream* 5 | *.swp 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | 45 | # Translations 46 | *.mo 47 | *.pot 48 | 49 | # Django stuff: 50 | *.log 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | # PyBuilder 56 | target/ 57 | 58 | # npm 59 | node_modules/ 60 | 61 | # diff 62 | *.diff 63 | 64 | # Ignore patch 65 | *.orig 66 | *.rej 67 | 68 | lextab.py 69 | yacctab.py 70 | .pytest_cache 71 | *~ 72 | certidude/static/coverage/ 73 | *.tar 74 | *.bz2 75 | *.gz 76 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: generic 3 | dist: trusty 4 | env: 5 | - COVERAGE_FILE=/tmp/.coverage 6 | after_success: 7 | - codecov 8 | script: 9 | - echo registry=http://registry.npmjs.org/ | sudo tee /root/.npmrc 10 | - sudo apt install software-properties-common python3-setuptools build-essential python3-dev libsasl2-dev libkrb5-dev 11 | - sudo apt remove python3-mimeparse 12 | - sudo mkdir -p /etc/systemd/system # Until Travis is stuck with 14.04 13 | - sudo easy_install3 pip 14 | - sudo -H pip3 install -r requirements.txt 15 | - sudo -H pip3 install codecov pytest-cov requests-kerberos 16 | - sudo -H pip3 install -e . 17 | - echo ca | sudo tee /etc/hostname 18 | - echo 127.0.0.1 localhost | sudo tee /etc/hosts 19 | - echo 127.0.1.1 ca.example.lan ca | sudo tee -a /etc/hosts 20 | - sudo hostname -F /etc/hostname 21 | - sudo find /home/ -type d -exec chmod 755 {} \; # Allow certidude serve to read templates 22 | - sudo coverage run --parallel-mode --source certidude -m py.test tests --capture=sys 23 | - sudo coverage combine 24 | - sudo coverage report 25 | - sudo coverage xml -i 26 | cache: pip 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Lauri Võsandi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include certidude/templates/*.sh 3 | include certidude/templates/*.service 4 | include certidude/templates/*.ovpn 5 | include certidude/templates/*.conf 6 | include certidude/templates/*.ini 7 | include certidude/templates/mail/*.md 8 | include certidude/templates/client/*.timer 9 | include certidude/templates/client/*.service 10 | include certidude/templates/server/*.service 11 | include certidude/templates/server/*.conf 12 | include certidude/static/js/*.js 13 | include certidude/static/css/*.css 14 | include certidude/static/fonts/*.woff2 15 | include certidude/static/img/*.png 16 | include certidude/static/views/*.html 17 | include certidude/static/snippets/*.sh 18 | include certidude/static/snippets/*.yml 19 | include certidude/static/snippets/*.mobileconfig 20 | include certidude/static/snippets/*.conf 21 | include certidude/static/snippets/*.ps1 22 | include certidude/static/*.html 23 | include certidude/static/robots.txt 24 | include certidude/sql/*/*.sql 25 | include certidude/builder/overlay/usr/bin/certidude-* 26 | include certidude/builder/overlay/etc/uci-defaults/* 27 | include certidude/builder/overlay/etc/hotplug.d/iface/50-certidude 28 | include certidude/builder/overlay/etc/profile 29 | include certidude/builder/*.sh 30 | -------------------------------------------------------------------------------- /certidude/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurivosandi/certidude/6e50c85c8587d3e800d01ce1d1588965c29b2c46/certidude/__init__.py -------------------------------------------------------------------------------- /certidude/api/__init__.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | import falcon 4 | import ipaddress 5 | import logging 6 | import os 7 | from certidude import config 8 | from certidude.common import drop_privileges 9 | from user_agents import parse 10 | from wsgiref.simple_server import make_server, WSGIServer 11 | from setproctitle import setproctitle 12 | 13 | class NormalizeMiddleware(object): 14 | def process_request(self, req, resp, *args): 15 | req.context["remote_addr"] = ipaddress.ip_address(req.access_route[0]) 16 | if req.user_agent: 17 | req.context["user_agent"] = parse(req.user_agent) 18 | else: 19 | req.context["user_agent"] = "Unknown user agent" 20 | 21 | 22 | class App(object): 23 | PORT = 8080 24 | FORKS = None 25 | DROP_PRIVILEGES = True 26 | 27 | def __init__(self): 28 | app = falcon.API(middleware=NormalizeMiddleware()) 29 | app.req_options.auto_parse_form_urlencoded = True 30 | self.attach(app) 31 | 32 | # Set up log handlers 33 | log_handlers = [] 34 | if config.LOGGING_BACKEND == "sql": 35 | from certidude.mysqllog import LogHandler 36 | from certidude.api.log import LogResource 37 | uri = config.cp.get("logging", "database") 38 | log_handlers.append(LogHandler(uri)) 39 | elif config.LOGGING_BACKEND == "syslog": 40 | from logging.handlers import SysLogHandler 41 | log_handlers.append(SysLogHandler()) 42 | # Browsing syslog via HTTP is obviously not possible out of the box 43 | elif config.LOGGING_BACKEND: 44 | raise ValueError("Invalid logging.backend = %s" % config.LOGGING_BACKEND) 45 | from certidude.push import EventSourceLogHandler 46 | log_handlers.append(EventSourceLogHandler()) 47 | 48 | for j in logging.Logger.manager.loggerDict.values(): 49 | if isinstance(j, logging.Logger): # PlaceHolder is what? 50 | if j.name.startswith("certidude."): 51 | j.setLevel(logging.DEBUG) 52 | for handler in log_handlers: 53 | j.addHandler(handler) 54 | 55 | self.server = make_server("127.0.1.1", self.PORT, app, WSGIServer) 56 | setproctitle("certidude: %s" % self.NAME) 57 | 58 | def run(self): 59 | if self.DROP_PRIVILEGES: 60 | drop_privileges() 61 | try: 62 | self.server.serve_forever() 63 | except KeyboardInterrupt: 64 | return 65 | else: 66 | return 67 | 68 | def fork(self): 69 | for j in range(self.FORKS): 70 | if not os.fork(): 71 | self.run() 72 | return True 73 | return False 74 | 75 | 76 | 77 | class ReadWriteApp(App): 78 | NAME = "backend server" 79 | 80 | def attach(self, app): 81 | from certidude import authority, config 82 | from certidude.tokens import TokenManager 83 | from .signed import SignedCertificateDetailResource 84 | from .request import RequestListResource, RequestDetailResource 85 | from .lease import LeaseResource, LeaseDetailResource 86 | from .script import ScriptResource 87 | from .tag import TagResource, TagDetailResource 88 | from .attrib import AttributeResource 89 | from .bootstrap import BootstrapResource 90 | from .token import TokenResource 91 | from .session import SessionResource, CertificateAuthorityResource 92 | from .revoked import RevokedCertificateDetailResource 93 | 94 | # Certificate authority API calls 95 | app.add_route("/api/certificate/", CertificateAuthorityResource()) 96 | app.add_route("/api/signed/{cn}/", SignedCertificateDetailResource(authority)) 97 | app.add_route("/api/request/{cn}/", RequestDetailResource(authority)) 98 | app.add_route("/api/request/", RequestListResource(authority)) 99 | app.add_route("/api/revoked/{serial_number}/", RevokedCertificateDetailResource(authority)) 100 | 101 | token_resource = None 102 | token_manager = None 103 | if config.USER_ENROLLMENT_ALLOWED: # TODO: add token enable/disable flag for config 104 | if config.TOKEN_BACKEND == "sql": 105 | token_manager = TokenManager(config.TOKEN_DATABASE) 106 | token_resource = TokenResource(authority, token_manager) 107 | app.add_route("/api/token/", token_resource) 108 | elif not config.TOKEN_BACKEND: 109 | pass 110 | else: 111 | raise NotImplementedError("Token backend '%s' not supported" % config.TOKEN_BACKEND) 112 | 113 | app.add_route("/api/", SessionResource(authority, token_manager)) 114 | 115 | # Extended attributes for scripting etc. 116 | app.add_route("/api/signed/{cn}/attr/", AttributeResource(authority, namespace="machine")) 117 | app.add_route("/api/signed/{cn}/script/", ScriptResource(authority)) 118 | 119 | # API calls used by pushed events on the JS end 120 | app.add_route("/api/signed/{cn}/tag/", TagResource(authority)) 121 | app.add_route("/api/signed/{cn}/lease/", LeaseDetailResource(authority)) 122 | 123 | # API call used to delete existing tags 124 | app.add_route("/api/signed/{cn}/tag/{tag}/", TagDetailResource(authority)) 125 | 126 | # Gateways can submit leases via this API call 127 | app.add_route("/api/lease/", LeaseResource(authority)) 128 | 129 | # Bootstrap resource 130 | app.add_route("/api/bootstrap/", BootstrapResource(authority)) 131 | 132 | # Add SCEP handler if we have any whitelisted subnets 133 | if config.SCEP_SUBNETS: 134 | from .scep import SCEPResource 135 | app.add_route("/api/scep/", SCEPResource(authority)) 136 | return app 137 | 138 | 139 | class ResponderApp(App): 140 | PORT = 8081 141 | FORKS = 4 142 | NAME = "ocsp responder" 143 | 144 | def attach(self, app): 145 | from certidude import authority 146 | from .ocsp import OCSPResource 147 | app.add_sink(OCSPResource(authority), prefix="/api/ocsp") 148 | return app 149 | 150 | 151 | class RevocationListApp(App): 152 | PORT = 8082 153 | FORKS = 2 154 | NAME = "crl server" 155 | 156 | def attach(self, app): 157 | from certidude import authority 158 | from .revoked import RevocationListResource 159 | app.add_route("/api/revoked/", RevocationListResource(authority)) 160 | return app 161 | 162 | 163 | class BuilderApp(App): 164 | PORT = 8083 165 | FORKS = 1 166 | NAME = "image builder" 167 | 168 | def attach(self, app): 169 | # LEDE image builder resource 170 | from certidude import authority 171 | from .builder import ImageBuilderResource 172 | app.add_route("/api/build/{profile}/{suggested_filename}", ImageBuilderResource()) 173 | return app 174 | 175 | 176 | class LogApp(App): 177 | PORT = 8084 178 | FORKS = 2 179 | NAME = "log server" 180 | 181 | def attach(self, app): 182 | from certidude.api.log import LogResource 183 | uri = config.cp.get("logging", "database") 184 | app.add_route("/api/log/", LogResource(uri)) 185 | return app 186 | -------------------------------------------------------------------------------- /certidude/api/attrib.py: -------------------------------------------------------------------------------- 1 | import falcon 2 | import logging 3 | import re 4 | from xattr import setxattr, listxattr, removexattr, getxattr 5 | from certidude import push 6 | from certidude.decorators import serialize, csrf_protection 7 | from .utils.firewall import login_required, authorize_admin, whitelist_subject 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | class AttributeResource(object): 12 | def __init__(self, authority, namespace): 13 | self.authority = authority 14 | self.namespace = namespace 15 | 16 | @serialize 17 | @login_required 18 | @authorize_admin 19 | def on_get(self, req, resp, cn): 20 | """ 21 | Return extended attributes stored on the server. 22 | This not only contains tags and lease information, 23 | but might also contain some other sensitive information. 24 | """ 25 | try: 26 | path, buf, cert, attribs = self.authority.get_attributes(cn, 27 | namespace=self.namespace, flat=True) 28 | except IOError: 29 | raise falcon.HTTPNotFound() 30 | else: 31 | return attribs 32 | 33 | @csrf_protection 34 | @whitelist_subject 35 | def on_post(self, req, resp, cn): 36 | namespace = ("user.%s." % self.namespace).encode("ascii") 37 | try: 38 | path, buf, cert, signed, expires = self.authority.get_signed(cn) 39 | except IOError: 40 | raise falcon.HTTPNotFound() 41 | else: 42 | for key in req.params: 43 | if not re.match("[a-z0-9_\.]+$", key): 44 | raise falcon.HTTPBadRequest("Invalid key %s" % key) 45 | valid = set() 46 | modified = False 47 | for key, value in req.params.items(): 48 | identifier = ("user.%s.%s" % (self.namespace, key)).encode("ascii") 49 | try: 50 | if getxattr(path, identifier).decode("utf-8") != value: 51 | modified = True 52 | except OSError: # no such attribute 53 | pass 54 | setxattr(path, identifier, value.encode("utf-8")) 55 | valid.add(identifier) 56 | for key in listxattr(path): 57 | if not key.startswith(namespace): 58 | continue 59 | if key not in valid: 60 | modified = True 61 | removexattr(path, key) 62 | if modified: 63 | push.publish("attribute-update", cn) 64 | 65 | -------------------------------------------------------------------------------- /certidude/api/bootstrap.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from certidude import config, const 3 | from jinja2 import Template 4 | from .utils import AuthorityHandler 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | class BootstrapResource(AuthorityHandler): 9 | def on_get(self, req, resp): 10 | resp.body = Template(open(config.BOOTSTRAP_TEMPLATE).read()).render( 11 | authority = const.FQDN, 12 | servers = self.authority.list_server_names()) 13 | 14 | -------------------------------------------------------------------------------- /certidude/api/builder.py: -------------------------------------------------------------------------------- 1 | 2 | import click 3 | import falcon 4 | import logging 5 | import os 6 | import subprocess 7 | from certidude import config, const, authority 8 | from certidude.common import cert_to_dn 9 | from ipaddress import ip_network 10 | from jinja2 import Template 11 | from .utils.firewall import login_required, authorize_admin 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | class ImageBuilderResource(object): 16 | @login_required 17 | @authorize_admin 18 | def on_get(self, req, resp, profile, suggested_filename): 19 | router = [j[0] for j in authority.list_signed( 20 | common_name=config.cp2.get(profile, "router"))][0] 21 | subnets = set([ip_network(j) for j in config.cp2.get(profile, "subnets").replace(",", " ").split(" ")]) 22 | model = config.cp2.get(profile, "model") 23 | build_script_path = config.cp2.get(profile, "command") 24 | overlay_path = config.cp2.get(profile, "overlay") 25 | site_script_path = config.cp2.get(profile, "script") 26 | suffix = config.cp2.get(profile, "filename") 27 | 28 | build = "/var/lib/certidude/builder/" + profile 29 | log_path = build + "/build.log" 30 | if not os.path.exists(build + "/overlay/etc/uci-defaults"): 31 | os.makedirs(build + "/overlay/etc/uci-defaults") 32 | os.system("rsync -av " + overlay_path + "/ " + build + "/overlay/") 33 | 34 | if site_script_path: 35 | template = Template(open(site_script_path).read()) 36 | with open(build + "/overlay/etc/uci-defaults/99-site-config", "w") as fh: 37 | fh.write(template.render(authority_name=const.FQDN)) 38 | 39 | proc = subprocess.Popen(("/bin/bash", build_script_path), 40 | stdout=open(log_path, "w"), stderr=subprocess.STDOUT, 41 | close_fds=True, shell=False, 42 | cwd=os.path.dirname(os.path.realpath(build_script_path)), 43 | env={"PROFILE": model, "PATH":"/usr/sbin:/usr/bin:/sbin:/bin", 44 | "ROUTER": router, 45 | "IKE": config.cp2.get(profile, "ike"), 46 | "ESP": config.cp2.get(profile, "esp"), 47 | "SUBNETS": ",".join(str(j) for j in subnets), 48 | "AUTHORITY_CERTIFICATE_ALGORITHM": authority.public_key.algorithm, 49 | "AUTHORITY_CERTIFICATE_DISTINGUISHED_NAME": cert_to_dn(authority.certificate), 50 | "BUILD":build, "OVERLAY":build + "/overlay/"}, 51 | startupinfo=None, creationflags=0) 52 | proc.communicate() 53 | if proc.returncode: 54 | logger.info("Build script finished with non-zero exitcode, see %s for more information" % log_path) 55 | raise falcon.HTTPInternalServerError("Build script finished with non-zero exitcode") 56 | 57 | for dname in os.listdir(build): 58 | if dname.startswith("openwrt-imagebuilder-") and ".tar." not in dname: 59 | subdir = os.path.join(build, dname, "bin", "targets") 60 | for root, dirs, files in os.walk(subdir): 61 | for filename in files: 62 | if filename.endswith(suffix): 63 | path = os.path.join(root, filename) 64 | click.echo("Serving: %s" % path) 65 | resp.body = open(path, "rb").read() 66 | resp.set_header("Content-Disposition", ("attachment; filename=%s" % suggested_filename)) 67 | return 68 | raise falcon.HTTPNotFound(description="Couldn't find file ending with '%s' in directory %s" % (suffix, subdir)) 69 | raise falcon.HTTPNotFound(description="Failed to find image builder directory in %s" % build) 70 | 71 | -------------------------------------------------------------------------------- /certidude/api/lease.py: -------------------------------------------------------------------------------- 1 | import falcon 2 | import logging 3 | import os 4 | import re 5 | import xattr 6 | from datetime import datetime 7 | from certidude import config, push 8 | from certidude.decorators import serialize 9 | from .utils import AuthorityHandler 10 | from .utils.firewall import login_required, authorize_admin, authorize_server 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | # TODO: lease namespacing (?) 15 | 16 | class LeaseDetailResource(AuthorityHandler): 17 | @serialize 18 | @login_required 19 | @authorize_admin 20 | def on_get(self, req, resp, cn): 21 | try: 22 | path, buf, cert, signed, expires = self.authority.get_signed(cn) 23 | return dict( 24 | last_seen = xattr.getxattr(path, "user.lease.last_seen").decode("ascii"), 25 | inner_address = xattr.getxattr(path, "user.lease.inner_address").decode("ascii"), 26 | outer_address = xattr.getxattr(path, "user.lease.outer_address").decode("ascii") 27 | ) 28 | except EnvironmentError: # Certificate or attribute not found 29 | raise falcon.HTTPNotFound() 30 | 31 | 32 | class LeaseResource(AuthorityHandler): 33 | @authorize_server 34 | def on_post(self, req, resp): 35 | client_common_name = req.get_param("client", required=True) 36 | m = re.match("^(.*, )*CN=(.+?)(, .*)*$", client_common_name) # It's actually DN, resolve it to CN 37 | if m: 38 | _, client_common_name, _ = m.groups() 39 | 40 | path, buf, cert, signed, expires = self.authority.get_signed(client_common_name) # TODO: catch exceptions 41 | if req.get_param("serial") and cert.serial_number != req.get_param_as_int("serial"): # OCSP-ish solution for OpenVPN, not exposed for StrongSwan 42 | logger.info("Gateway %s attempted to submit lease information for %s with expired/unknown serial %x, expected %x" % ( 43 | req.context["machine"], client_common_name, 44 | req.get_param_as_int("serial"), cert.serial_number)) 45 | raise falcon.HTTPForbidden("Forbidden", "Invalid serial number supplied") 46 | now = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" 47 | 48 | xattr.setxattr(path, "user.lease.outer_address", req.get_param("outer_address", required=True).encode("ascii")) 49 | xattr.setxattr(path, "user.lease.inner_address", req.get_param("inner_address", required=True).encode("ascii")) 50 | xattr.setxattr(path, "user.lease.last_seen", now) 51 | push.publish("lease-update", client_common_name) 52 | 53 | server_common_name = req.context.get("machine") 54 | path = os.path.join(config.SIGNED_DIR, server_common_name + ".pem") 55 | xattr.setxattr(path, "user.lease.outer_address", "") 56 | xattr.setxattr(path, "user.lease.inner_address", "%s" % req.context.get("remote_addr")) 57 | xattr.setxattr(path, "user.lease.last_seen", now) 58 | push.publish("lease-update", server_common_name) 59 | 60 | # client-disconnect is pretty much unusable: 61 | # - Android Connect Client results "IP packet with unknown IP version=2" on gateway 62 | # - NetworkManager just kills OpenVPN client, disconnect is never reported 63 | # - Disconnect is also not reported when uplink connection dies or laptop goes to sleep 64 | -------------------------------------------------------------------------------- /certidude/api/log.py: -------------------------------------------------------------------------------- 1 | 2 | from certidude.decorators import serialize 3 | from certidude.relational import RelationalMixin 4 | from .utils.firewall import login_required, authorize_admin 5 | 6 | class LogResource(RelationalMixin): 7 | SQL_CREATE_TABLES = "log_tables.sql" 8 | 9 | @serialize 10 | @login_required 11 | @authorize_admin 12 | def on_get(self, req, resp): 13 | # TODO: Add last id parameter 14 | return self.iterfetch("select * from log order by created desc limit ?", 15 | req.get_param_as_int("limit", required=True)) 16 | -------------------------------------------------------------------------------- /certidude/api/ocsp.py: -------------------------------------------------------------------------------- 1 | import falcon 2 | import logging 3 | import os 4 | from asn1crypto.util import timezone 5 | from asn1crypto import ocsp 6 | from base64 import b64decode 7 | from certidude import config, const 8 | from datetime import datetime, timedelta 9 | from oscrypto import asymmetric 10 | from .utils import AuthorityHandler 11 | from .utils.firewall import whitelist_subnets 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | class OCSPResource(AuthorityHandler): 16 | @whitelist_subnets(config.OCSP_SUBNETS) 17 | def __call__(self, req, resp): 18 | try: 19 | if req.method == "GET": 20 | _, _, _, tail = req.path.split("/", 3) 21 | body = b64decode(tail) 22 | elif req.method == "POST": 23 | body = req.stream.read(req.content_length or 0) 24 | else: 25 | raise falcon.HTTPMethodNotAllowed() 26 | ocsp_req = ocsp.OCSPRequest.load(body) 27 | except ValueError: 28 | raise falcon.HTTPBadRequest() 29 | 30 | fh = open(config.AUTHORITY_CERTIFICATE_PATH, "rb") # TODO: import from authority 31 | server_certificate = asymmetric.load_certificate(fh.read()) 32 | fh.close() 33 | 34 | now = datetime.now(timezone.utc) 35 | response_extensions = [] 36 | 37 | try: 38 | for ext in ocsp_req["tbs_request"]["request_extensions"]: 39 | if ext["extn_id"].native == "nonce": 40 | response_extensions.append( 41 | ocsp.ResponseDataExtension({ 42 | 'extn_id': "nonce", 43 | 'critical': False, 44 | 'extn_value': ext["extn_value"] 45 | }) 46 | ) 47 | except ValueError: # https://github.com/wbond/asn1crypto/issues/56 48 | pass 49 | 50 | responses = [] 51 | for item in ocsp_req["tbs_request"]["request_list"]: 52 | serial = item["req_cert"]["serial_number"].native 53 | assert serial > 0, "Serial number correctness check failed" 54 | 55 | try: 56 | link_target = os.readlink(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%040x.pem" % serial)) 57 | assert link_target.startswith("../") 58 | assert link_target.endswith(".pem") 59 | path, buf, cert, signed, expires = self.authority.get_signed(link_target[3:-4]) 60 | if serial != cert.serial_number: 61 | logger.error("Certificate store integrity check failed, %s refers to certificate with serial %040x", link_target, cert.serial_number) 62 | raise EnvironmentError("Integrity check failed") 63 | logger.debug("OCSP responder queried from %s for %s with serial %040x, returned status 'good'", 64 | req.context.get("remote_addr"), cert.subject.native["common_name"], serial) 65 | status = ocsp.CertStatus(name='good', value=None) 66 | except EnvironmentError: 67 | try: 68 | path, buf, cert, signed, expires, revoked, reason = self.authority.get_revoked(serial) 69 | logger.debug("OCSP responder queried from %s for %s with serial %040x, returned status 'revoked' due to %s", 70 | req.context.get("remote_addr"), cert.subject.native["common_name"], serial, reason) 71 | status = ocsp.CertStatus( 72 | name='revoked', 73 | value={ 74 | 'revocation_time': revoked, 75 | 'revocation_reason': reason, 76 | }) 77 | except EnvironmentError: 78 | logger.info("OCSP responder queried for unknown serial %040x from %s", serial, req.context.get("remote_addr")) 79 | status = ocsp.CertStatus(name="unknown", value=None) 80 | 81 | responses.append({ 82 | 'cert_id': { 83 | 'hash_algorithm': { 84 | 'algorithm': "sha1" 85 | }, 86 | 'issuer_name_hash': server_certificate.asn1.subject.sha1, 87 | 'issuer_key_hash': server_certificate.public_key.asn1.sha1, 88 | 'serial_number': serial, 89 | }, 90 | 'cert_status': status, 91 | 'this_update': now - const.CLOCK_SKEW_TOLERANCE, 92 | 'next_update': now + timedelta(minutes=15) + const.CLOCK_SKEW_TOLERANCE, 93 | 'single_extensions': [] 94 | }) 95 | 96 | response_data = ocsp.ResponseData({ 97 | 'responder_id': ocsp.ResponderId(name='by_key', value=server_certificate.public_key.asn1.sha1), 98 | 'produced_at': now, 99 | 'responses': responses, 100 | 'response_extensions': response_extensions 101 | }) 102 | 103 | resp.body = ocsp.OCSPResponse({ 104 | 'response_status': "successful", 105 | 'response_bytes': { 106 | 'response_type': "basic_ocsp_response", 107 | 'response': { 108 | 'tbs_response_data': response_data, 109 | 'certs': [server_certificate.asn1], 110 | 'signature_algorithm': {'algorithm': "sha1_ecdsa" if self.authority.public_key.algorithm == "ec" else "sha1_rsa" }, 111 | 'signature': (asymmetric.ecdsa_sign if self.authority.public_key.algorithm == "ec" else asymmetric.rsa_pkcs1v15_sign)( 112 | self.authority.private_key, 113 | response_data.dump(), 114 | "sha1" 115 | ) 116 | } 117 | } 118 | }).dump() 119 | 120 | # Interestingly openssl's OCSP code doesn't care about content type 121 | resp.append_header("Content-Type", "application/ocsp-response") 122 | 123 | -------------------------------------------------------------------------------- /certidude/api/revoked.py: -------------------------------------------------------------------------------- 1 | import falcon 2 | import logging 3 | from certidude import const, config 4 | from .utils import AuthorityHandler 5 | from .utils.firewall import whitelist_subnets 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | class RevocationListResource(AuthorityHandler): 10 | @whitelist_subnets(config.CRL_SUBNETS) 11 | def on_get(self, req, resp): 12 | # Primarily offer DER encoded CRL as per RFC5280 13 | # This is also what StrongSwan expects 14 | if req.client_accepts("application/x-pkcs7-crl"): 15 | resp.set_header("Content-Type", "application/x-pkcs7-crl") 16 | resp.append_header( 17 | "Content-Disposition", 18 | ("attachment; filename=%s.crl" % const.HOSTNAME)) 19 | # Convert PEM to DER 20 | logger.debug("Serving revocation list (DER) to %s", req.context.get("remote_addr")) 21 | resp.body = self.authority.export_crl(pem=False) 22 | elif req.client_accepts("application/x-pem-file"): 23 | resp.set_header("Content-Type", "application/x-pem-file") 24 | resp.append_header( 25 | "Content-Disposition", 26 | ("attachment; filename=%s-crl.pem" % const.HOSTNAME)) 27 | logger.debug("Serving revocation list (PEM) to %s", req.context.get("remote_addr")) 28 | resp.body = self.authority.export_crl() 29 | else: 30 | logger.debug("Client %s asked revocation list in unsupported format" % req.context.get("remote_addr")) 31 | raise falcon.HTTPUnsupportedMediaType( 32 | "Client did not accept application/x-pkcs7-crl or application/x-pem-file") 33 | 34 | 35 | class RevokedCertificateDetailResource(AuthorityHandler): 36 | def on_get(self, req, resp, serial_number): 37 | try: 38 | path, buf, cert, signed, expires, revoked, reason = self.authority.get_revoked(serial_number) 39 | except EnvironmentError: 40 | logger.warning("Failed to serve non-existant revoked certificate with serial %s to %s", 41 | serial_number, req.context.get("remote_addr")) 42 | raise falcon.HTTPNotFound() 43 | resp.set_header("Content-Type", "application/x-pem-file") 44 | resp.set_header("Content-Disposition", ("attachment; filename=%x.pem" % cert.serial_number)) 45 | resp.body = buf 46 | logger.debug("Served revoked certificate with serial %s to %s", 47 | serial_number, req.context.get("remote_addr")) 48 | -------------------------------------------------------------------------------- /certidude/api/script.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from certidude import const, config 4 | from jinja2 import Environment, FileSystemLoader 5 | from .utils import AuthorityHandler 6 | from .utils.firewall import whitelist_subject 7 | 8 | logger = logging.getLogger(__name__) 9 | env = Environment(loader=FileSystemLoader(config.SCRIPT_DIR), trim_blocks=True) 10 | 11 | class ScriptResource(AuthorityHandler): 12 | @whitelist_subject 13 | def on_get(self, req, resp, cn): 14 | path, buf, cert, attribs = self.authority.get_attributes(cn) 15 | # TODO: are keys unique? 16 | named_tags = {} 17 | other_tags = [] 18 | 19 | try: 20 | for tag in attribs.get("user").get("xdg").get("tags").split(","): 21 | if "=" in tag: 22 | k, v = tag.split("=", 1) 23 | named_tags[k] = v 24 | else: 25 | other_tags.append(tag) 26 | except AttributeError: # No tags 27 | pass 28 | 29 | script = named_tags.get("script", "default.sh") 30 | assert script in os.listdir(config.SCRIPT_DIR) 31 | resp.set_header("Content-Type", "text/x-shellscript") 32 | resp.body = env.get_template(os.path.join(script)).render( 33 | authority_name=const.FQDN, 34 | common_name=cn, 35 | other_tags=other_tags, 36 | named_tags=named_tags, 37 | attributes=attribs.get("user").get("machine")) 38 | logger.info("Served script %s for %s at %s" % (script, cn, req.context["remote_addr"])) 39 | # TODO: Assert time is within reasonable range 40 | -------------------------------------------------------------------------------- /certidude/api/signed.py: -------------------------------------------------------------------------------- 1 | 2 | import falcon 3 | import logging 4 | import json 5 | import hashlib 6 | from certidude.decorators import csrf_protection 7 | from xattr import listxattr, getxattr 8 | from .utils import AuthorityHandler 9 | from .utils.firewall import login_required, authorize_admin 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | class SignedCertificateDetailResource(AuthorityHandler): 14 | def on_get(self, req, resp, cn): 15 | 16 | preferred_type = req.client_prefers(("application/json", "application/x-pem-file")) 17 | try: 18 | path, buf, cert, signed, expires = self.authority.get_signed(cn) 19 | except EnvironmentError: 20 | logger.warning("Failed to serve non-existant certificate %s to %s", 21 | cn, req.context.get("remote_addr")) 22 | raise falcon.HTTPNotFound() 23 | 24 | if preferred_type == "application/x-pem-file": 25 | resp.set_header("Content-Type", "application/x-pem-file") 26 | resp.set_header("Content-Disposition", ("attachment; filename=%s.pem" % cn)) 27 | resp.body = buf 28 | logger.debug("Served certificate %s to %s as application/x-pem-file", 29 | cn, req.context.get("remote_addr")) 30 | elif preferred_type == "application/json": 31 | resp.set_header("Content-Type", "application/json") 32 | resp.set_header("Content-Disposition", ("attachment; filename=%s.json" % cn)) 33 | try: 34 | signer_username = getxattr(path, "user.signature.username").decode("ascii") 35 | except IOError: 36 | signer_username = None 37 | 38 | attributes = {} 39 | for key in listxattr(path): 40 | if key.startswith(b"user.machine."): 41 | attributes[key[13:].decode("ascii")] = getxattr(path, key).decode("ascii") 42 | 43 | # TODO: dedup 44 | resp.body = json.dumps(dict( 45 | common_name = cn, 46 | signer = signer_username, 47 | serial = "%040x" % cert.serial_number, 48 | organizational_unit = cert.subject.native.get("organizational_unit_name"), 49 | signed = cert["tbs_certificate"]["validity"]["not_before"].native.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z", 50 | expires = cert["tbs_certificate"]["validity"]["not_after"].native.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z", 51 | sha256sum = hashlib.sha256(buf).hexdigest(), 52 | attributes = attributes or None, 53 | lease = None, 54 | extensions = dict([ 55 | (e["extn_id"].native, e["extn_value"].native) 56 | for e in cert["tbs_certificate"]["extensions"] 57 | if e["extn_id"].native in ("extended_key_usage",)]) 58 | 59 | )) 60 | logger.debug("Served certificate %s to %s as application/json", 61 | cn, req.context.get("remote_addr")) 62 | else: 63 | logger.debug("Client did not accept application/json or application/x-pem-file") 64 | raise falcon.HTTPUnsupportedMediaType( 65 | "Client did not accept application/json or application/x-pem-file") 66 | 67 | @csrf_protection 68 | @login_required 69 | @authorize_admin 70 | def on_delete(self, req, resp, cn): 71 | self.authority.revoke(cn, 72 | reason=req.get_param("reason", default="key_compromise"), 73 | user=req.context.get("user") 74 | ) 75 | 76 | -------------------------------------------------------------------------------- /certidude/api/tag.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from xattr import getxattr, removexattr, setxattr 3 | from certidude import push 4 | from certidude.decorators import serialize, csrf_protection 5 | from .utils import AuthorityHandler 6 | from .utils.firewall import login_required, authorize_admin 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | class TagResource(AuthorityHandler): 11 | @serialize 12 | @login_required 13 | @authorize_admin 14 | def on_get(self, req, resp, cn): 15 | path, buf, cert, signed, expires = self.authority.get_signed(cn) 16 | tags = [] 17 | try: 18 | for tag in getxattr(path, "user.xdg.tags").decode("utf-8").split(","): 19 | if "=" in tag: 20 | k, v = tag.split("=", 1) 21 | else: 22 | k, v = "other", tag 23 | tags.append(dict(id=tag, key=k, value=v)) 24 | except IOError: # No user.xdg.tags attribute 25 | pass 26 | return tags 27 | 28 | 29 | @csrf_protection 30 | @login_required 31 | @authorize_admin 32 | def on_post(self, req, resp, cn): 33 | path, buf, cert, signed, expires = self.authority.get_signed(cn) 34 | key, value = req.get_param("key", required=True), req.get_param("value", required=True) 35 | try: 36 | tags = set(getxattr(path, "user.xdg.tags").decode("utf-8").split(",")) 37 | except IOError: 38 | tags = set() 39 | if key == "other": 40 | tags.add(value) 41 | else: 42 | tags.add("%s=%s" % (key,value)) 43 | setxattr(path, "user.xdg.tags", ",".join(tags).encode("utf-8")) 44 | logger.info("Tag %s=%s set for %s by %s" % (key, value, cn, req.context.get("user"))) 45 | push.publish("tag-update", cn) 46 | 47 | 48 | class TagDetailResource(object): 49 | def __init__(self, authority): 50 | self.authority = authority 51 | 52 | @csrf_protection 53 | @login_required 54 | @authorize_admin 55 | def on_put(self, req, resp, cn, tag): 56 | path, buf, cert, signed, expires = self.authority.get_signed(cn) 57 | value = req.get_param("value", required=True) 58 | try: 59 | tags = set(getxattr(path, "user.xdg.tags").decode("utf-8").split(",")) 60 | except IOError: 61 | tags = set() 62 | try: 63 | tags.remove(tag) 64 | except KeyError: 65 | pass 66 | if "=" in tag: 67 | tags.add("%s=%s" % (tag.split("=")[0], value)) 68 | else: 69 | tags.add(value) 70 | setxattr(path, "user.xdg.tags", ",".join(tags).encode("utf-8")) 71 | logger.info("Tag %s set to %s for %s by %s" % (tag, value, cn, req.context.get("user"))) 72 | push.publish("tag-update", cn) 73 | 74 | @csrf_protection 75 | @login_required 76 | @authorize_admin 77 | def on_delete(self, req, resp, cn, tag): 78 | path, buf, cert, signed, expires = self.authority.get_signed(cn) 79 | tags = set(getxattr(path, "user.xdg.tags").decode("utf-8").split(",")) 80 | tags.remove(tag) 81 | if not tags: 82 | removexattr(path, "user.xdg.tags") 83 | else: 84 | setxattr(path, "user.xdg.tags", ",".join(tags)) 85 | logger.info("Tag %s removed for %s by %s" % (tag, cn, req.context.get("user"))) 86 | push.publish("tag-update", cn) 87 | -------------------------------------------------------------------------------- /certidude/api/token.py: -------------------------------------------------------------------------------- 1 | import click 2 | import codecs 3 | import falcon 4 | import logging 5 | import os 6 | import string 7 | from asn1crypto import pem 8 | from asn1crypto.csr import CertificationRequest 9 | from datetime import datetime, timedelta 10 | from time import time 11 | from certidude import mailer, const 12 | from certidude.tokens import TokenManager 13 | from certidude.relational import RelationalMixin 14 | from certidude.decorators import serialize 15 | from certidude.user import User 16 | from certidude import config 17 | from .utils import AuthorityHandler 18 | from .utils.firewall import login_required, authorize_admin 19 | 20 | logger = logging.getLogger(__name__) 21 | 22 | class TokenResource(AuthorityHandler): 23 | def __init__(self, authority, manager): 24 | AuthorityHandler.__init__(self, authority) 25 | self.manager = manager 26 | 27 | def on_put(self, req, resp): 28 | try: 29 | username, mail, created, expires, profile = self.manager.consume(req.get_param("token", required=True)) 30 | except RelationalMixin.DoesNotExist: 31 | raise falcon.HTTPForbidden("Forbidden", "No such token or token expired") 32 | body = req.stream.read(req.content_length) 33 | header, _, der_bytes = pem.unarmor(body) 34 | csr = CertificationRequest.load(der_bytes) 35 | common_name = csr["certification_request_info"]["subject"].native["common_name"] 36 | if not common_name.startswith(username + "@"): 37 | raise falcon.HTTPBadRequest("Bad requst", "Invalid common name %s" % common_name) 38 | try: 39 | _, resp.body = self.authority._sign(csr, body, profile=config.PROFILES.get(profile), 40 | overwrite=config.TOKEN_OVERWRITE_PERMITTED) 41 | resp.set_header("Content-Type", "application/x-pem-file") 42 | logger.info("Autosigned %s as proven by token ownership", common_name) 43 | except FileExistsError: 44 | logger.info("Won't autosign duplicate %s", common_name) 45 | raise falcon.HTTPConflict( 46 | "Certificate with such common name (CN) already exists", 47 | "Will not overwrite existing certificate signing request, explicitly delete existing one and try again") 48 | 49 | 50 | @serialize 51 | @login_required 52 | @authorize_admin 53 | def on_post(self, req, resp): 54 | self.manager.issue( 55 | issuer = req.context.get("user"), 56 | subject = User.objects.get(req.get_param("username", required=True)), 57 | subject_mail = req.get_param("mail")) 58 | -------------------------------------------------------------------------------- /certidude/api/utils/__init__.py: -------------------------------------------------------------------------------- 1 | class AuthorityHandler: 2 | def __init__(self, authority): 3 | self.authority = authority 4 | -------------------------------------------------------------------------------- /certidude/builder/ap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | source common.sh 4 | 5 | sed -e 's/trigger wan/trigger lan/' -i $OVERLAY/etc/config/certidude 6 | 7 | cat << \EOF > $OVERLAY/etc/uci-defaults/40-hostname 8 | 9 | MODEL=$(cat /etc/board.json | jsonfilter -e '@["model"]["id"]') 10 | 11 | # Hostname prefix 12 | case $MODEL in 13 | tl-*|archer-*) VENDOR=tplink ;; 14 | cf-*) VENDOR=comfast ;; 15 | *) VENDOR=ap ;; 16 | esac 17 | 18 | # Network interface with relevant MAC address 19 | case $MODEL in 20 | tl-wdr*) NIC=wlan1 ;; 21 | archer-*) NIC=eth1 ;; 22 | cf-e380ac-v2) NIC=eth0 ;; 23 | *) NIC=wlan0 ;; 24 | esac 25 | 26 | HOSTNAME=$VENDOR-$(cat /sys/class/net/$NIC/address | cut -d : -f 4- | sed -e 's/://g') 27 | uci set system.@system[0].hostname=$HOSTNAME 28 | uci set network.lan.hostname=$HOSTNAME 29 | 30 | EOF 31 | 32 | cat << \EOF > $OVERLAY/etc/uci-defaults/50-access-point 33 | 34 | # Remove firewall rules since AP bridges ethernet to wireless anyway 35 | uci delete firewall.@zone[1] 36 | uci delete firewall.@zone[0] 37 | uci delete firewall.@forwarding[0] 38 | for j in $(seq 0 10); do uci delete firewall.@rule[0]; done 39 | 40 | # Remove WAN interface 41 | uci delete network.wan 42 | uci delete network.wan6 43 | 44 | # Reconfigure DHCP client for bridge over LAN and WAN ports 45 | uci delete network.lan.ipaddr 46 | uci delete network.lan.netmask 47 | uci delete network.lan.ip6assign 48 | uci delete network.globals.ula_prefix 49 | uci delete network.@switch_vlan[1] 50 | uci delete dhcp.@dnsmasq[0].domain 51 | uci set network.lan.proto=dhcp 52 | uci set network.lan.ipv6=0 53 | uci set network.lan.ifname='eth0' 54 | uci set network.lan.stp=1 55 | 56 | # Radio ordering differs among models 57 | case $(uci get wireless.radio0.hwmode) in 58 | 11a) uci rename wireless.radio0=radio5ghz;; 59 | 11g) uci rename wireless.radio0=radio2ghz;; 60 | esac 61 | case $(uci get wireless.radio1.hwmode) in 62 | 11a) uci rename wireless.radio1=radio5ghz;; 63 | 11g) uci rename wireless.radio1=radio2ghz;; 64 | esac 65 | 66 | # Reset virtual SSID-s 67 | uci delete wireless.@wifi-iface[1] 68 | uci delete wireless.@wifi-iface[0] 69 | 70 | # Pseudorandomize channel selection, should work with 80MHz on 5GHz band 71 | case $(uci get system.@system[0].hostname | md5sum) in 72 | 1*|2*|3*|4*) uci set wireless.radio2ghz.channel=1; uci set wireless.radio5ghz.channel=36 ;; 73 | 5*|6*|7*|8*) uci set wireless.radio2ghz.channel=5; uci set wireless.radio5ghz.channel=52 ;; 74 | 9*|0*|a*|b*) uci set wireless.radio2ghz.channel=9; uci set wireless.radio5ghz.channel=100 ;; 75 | c*|d*|e*|f*) uci set wireless.radio2ghz.channel=13; uci set wireless.radio5ghz.channel=132 ;; 76 | esac 77 | 78 | # Create bridge for guests 79 | uci set network.guest=interface 80 | uci set network.guest.proto='static' 81 | uci set network.guest.address='0.0.0.0' 82 | uci set network.guest.type='bridge' 83 | uci set network.guest.ifname='eth0.156' # tag id 156 for guest network 84 | uci set network.guest.ipaddr='0.0.0.0' 85 | uci set network.guest.ipv6=0 86 | uci set network.guest.stp=1 87 | 88 | # Add VPN interface for IPSec 89 | uci set network.vpn=interface 90 | uci set network.vpn.ifname='ipsec0' 91 | uci set network.vpn.proto='none' 92 | 93 | uci set firewall.vpn=zone 94 | uci set firewall.vpn.name="vpn" 95 | uci set firewall.vpn.input="ACCEPT" 96 | uci set firewall.vpn.forward="ACCEPT" 97 | uci set firewall.vpn.output="ACCEPT" 98 | uci set firewall.vpn.network="vpn" 99 | 100 | # Disable switch tagging and bridge all ports on TP-Link WDR3600/WDR4300 101 | case $(cat /etc/board.json | jsonfilter -e '@["model"]["id"]') in 102 | tl-wdr*|archer*) 103 | uci set network.@switch[0].enable_vlan=0 104 | uci set network.@switch_vlan[0].ports='0 1 2 3 4 5 6' 105 | ;; 106 | *) ;; 107 | esac 108 | 109 | EOF 110 | 111 | make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="luci \ 112 | openssl-util curl ca-certificates dropbear \ 113 | strongswan-mod-kernel-libipsec kmod-tun strongswan-default strongswan-mod-openssl strongswan-mod-curl strongswan-mod-ccm strongswan-mod-gcm \ 114 | htop iftop netdata -odhcp6c -odhcpd -dnsmasq \ 115 | -luci-app-firewall \ 116 | -pppd -luci-proto-ppp -kmod-ppp -ppp -ppp-mod-pppoe \ 117 | -kmod-ip6tables -ip6tables -luci-proto-ipv6 -kmod-iptunnel6 -kmod-ipsec6" 118 | 119 | -------------------------------------------------------------------------------- /certidude/builder/common.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -x 5 | umask 022 6 | 7 | VERSION=18.06.1 8 | BASENAME=openwrt-imagebuilder-$VERSION-ar71xx-generic.Linux-x86_64 9 | FILENAME=$BASENAME.tar.xz 10 | URL=http://downloads.openwrt.org/releases/$VERSION/targets/ar71xx/generic/$FILENAME 11 | 12 | # curl of vanilla OpenWrt a the moment 13 | # - doesn't support ECDSA 14 | # - is compiled against embedded TLS library which doesn't support OCSP 15 | BASENAME=openwrt-imagebuilder-ar71xx-generic.Linux-x86_64 16 | FILENAME=$BASENAME.tar.xz 17 | URL=https://www.koodur.com/$FILENAME 18 | 19 | if [ ! -e $BUILD/$FILENAME ]; then 20 | wget -q $URL -O $BUILD/$FILENAME 21 | fi 22 | 23 | if [ ! -e $BUILD/$BASENAME ]; then 24 | tar xf $BUILD/$FILENAME -C $BUILD 25 | fi 26 | 27 | # Copy CA certificate 28 | AUTHORITY=$(hostname -f) 29 | 30 | mkdir -p $OVERLAY/etc/config 31 | mkdir -p $OVERLAY/etc/uci-defaults 32 | mkdir -p $OVERLAY/etc/certidude/authority/$AUTHORITY/ 33 | cp /var/lib/certidude/ca_cert.pem $OVERLAY/etc/certidude/authority/$AUTHORITY/ 34 | 35 | cat < $OVERLAY/etc/config/certidude 36 | 37 | config authority 38 | option gateway "$ROUTER" 39 | option hostname "$AUTHORITY" 40 | option trigger wan 41 | option key_type $AUTHORITY_CERTIFICATE_ALGORITHM 42 | option key_length 2048 43 | option key_curve secp384r1 44 | 45 | EOF 46 | 47 | case $AUTHORITY_CERTIFICATE_ALGORITHM in 48 | rsa) 49 | echo ": RSA /etc/certidude/authority/$AUTHORITY/host_key.pem" >> $OVERLAY/etc/ipsec.secrets 50 | ;; 51 | ec) 52 | echo ": ECDSA /etc/certidude/authority/$AUTHORITY/host_key.pem" >> $OVERLAY/etc/ipsec.secrets 53 | ;; 54 | *) 55 | echo "Unknown algorithm $AUTHORITY_CERTIFICATE_ALGORITHM" 56 | exit 1 57 | ;; 58 | esac 59 | 60 | cat << EOF > $OVERLAY/etc/certidude/authority/$AUTHORITY/updown 61 | #!/bin/sh 62 | 63 | CURL="curl -m 3 -f --key /etc/certidude/authority/$AUTHORITY/host_key.pem --cert /etc/certidude/authority/$AUTHORITY/host_cert.pem --cacert /etc/certidude/authority/$AUTHORITY/ca_cert.pem --cert-status" 64 | URL="https://$AUTHORITY:8443/api/signed/\$(uci get system.@system[0].hostname)/script/" 65 | 66 | case \$PLUTO_VERB in 67 | up-client) 68 | logger -t certidude -s "Downloading and executing \$URL" 69 | \$CURL \$URL -o /tmp/script.sh && sh /tmp/script.sh 70 | ;; 71 | *) ;; 72 | esac 73 | EOF 74 | 75 | chmod +x $OVERLAY/etc/certidude/authority/$AUTHORITY/updown 76 | 77 | cat << EOF > $OVERLAY/etc/ipsec.conf 78 | 79 | config setup 80 | strictcrlpolicy=yes 81 | 82 | ca $AUTHORITY 83 | auto=add 84 | cacert=/etc/certidude/authority/$AUTHORITY/ca_cert.pem 85 | # OCSP and CRL URL-s embedded in certificates 86 | 87 | conn %default 88 | keyingtries=%forever 89 | dpdaction=restart 90 | closeaction=restart 91 | ike=$IKE 92 | esp=$ESP 93 | left=%defaultroute 94 | leftcert=/etc/certidude/authority/$AUTHORITY/host_cert.pem 95 | leftca="$AUTHORITY_CERTIFICATE_DISTINGUISHED_NAME" 96 | rightca="$AUTHORITY_CERTIFICATE_DISTINGUISHED_NAME" 97 | 98 | conn c2s 99 | auto=start 100 | right="$ROUTER" 101 | rightsubnet="$SUBNETS" 102 | leftsourceip=%config 103 | leftupdown=/etc/certidude/authority/$AUTHORITY/updown 104 | 105 | EOF 106 | 107 | # Note that auto=route is not supported at the moment with libipsec 108 | -------------------------------------------------------------------------------- /certidude/builder/ipcam.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . common.sh 4 | 5 | cat << \EOF > $OVERLAY/etc/uci-defaults/40-hostname 6 | 7 | HOSTNAME=cam-$(cat /sys/class/net/eth0/address | cut -d : -f 4- | sed -e 's/://g') 8 | uci set system.@system[0].hostname=$HOSTNAME 9 | uci set network.wan.hostname=$HOSTNAME 10 | 11 | EOF 12 | 13 | touch $OVERLAY/etc/config/wireless 14 | 15 | cat << EOF > $OVERLAY/etc/uci-defaults/50-ipcam 16 | 17 | uci delete network.lan 18 | uci delete network.wan6 19 | 20 | uci set network.vpn=interface 21 | uci set network.vpn.ifname='ipsec0' 22 | uci set network.vpn.proto='none' 23 | uci set firewall.@zone[0].network=vpn 24 | uci delete firewall.@forwarding[0] 25 | 26 | uci set mjpg-streamer.core.enabled=1 27 | uci set mjpg-streamer.core.quality='' 28 | uci set mjpg-streamer.core.resolution='1280x720' 29 | uci delete mjpg-streamer.core.username 30 | uci delete mjpg-streamer.core.password 31 | 32 | uci certidude.@authority[0].red_led='gl-connect:red:wlan' 33 | uci certidude.@authority[0].green_led='gl-connect:green:lan' 34 | 35 | /etc/init.d/dropbear disable 36 | /etc/init.d/ipsec disable 37 | 38 | EOF 39 | 40 | 41 | make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="openssl-util curl ca-certificates \ 42 | strongswan-default strongswan-mod-openssl strongswan-mod-curl strongswan-mod-ccm strongswan-mod-gcm htop \ 43 | iftop tcpdump nmap nano usbutils luci luci-app-mjpg-streamer kmod-video-uvc dropbear \ 44 | -pppd -luci-proto-ppp -kmod-ppp -ppp -ppp-mod-pppoe \ 45 | -dnsmasq -odhcpd -odhcp6c -kmod-ath9k picocom strongswan-mod-kernel-libipsec kmod-tun \ 46 | netdata" 47 | -------------------------------------------------------------------------------- /certidude/builder/mfp.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . common.sh 4 | 5 | cat << \EOF > $OVERLAY/etc/uci-defaults/40-hostname 6 | 7 | HOSTNAME=mfp-$(cat /sys/class/net/eth0/address | cut -d : -f 4- | sed -e 's/://g') 8 | uci set system.@system[0].hostname=$HOSTNAME 9 | uci set network.wan.hostname=$HOSTNAME 10 | 11 | EOF 12 | 13 | mkdir -p $OVERLAY/etc/config/ 14 | touch $OVERLAY/etc/config/wireless 15 | 16 | cat << EOF > $OVERLAY/etc/uci-defaults/50-mfp 17 | 18 | # Disable rebind protection for DNS 19 | uci set dhcp.@dnsmasq[0].rebind_protection=0 20 | uci set dhcp.@dnsmasq[0].domain='mfp.lan' 21 | uci delete dhcp.@dnsmasq[0].local 22 | 23 | # Disable bridge for LAN since WiFi is disabled 24 | uci delete network.lan.type 25 | uci set dhcp.lan.limit=1 26 | 27 | uci set network.vpn=interface 28 | uci set network.vpn.ifname='ipsec0' 29 | uci set network.vpn.proto='none' 30 | 31 | uci set firewall.vpn=zone 32 | uci set firewall.vpn.name="vpn" 33 | uci set firewall.vpn.input="ACCEPT" 34 | uci set firewall.vpn.forward="ACCEPT" 35 | uci set firewall.vpn.output="ACCEPT" 36 | uci set firewall.vpn.network="vpn" 37 | uci set firewall.vpn.masq='1' 38 | 39 | uci set firewall.lan2vpn=forwarding 40 | uci set firewall.lan2vpn.src='lan' 41 | uci set firewall.lan2vpn.dest='vpn' 42 | 43 | uci set firewall.allow_ipp=redirect 44 | uci set firewall.allow_ipp.name="Allow-IPP-on-MFP" 45 | uci set firewall.allow_ipp.src=vpn 46 | uci set firewall.allow_ipp.src_dport=631 47 | uci set firewall.allow_ipp.dest=lan 48 | uci set firewall.allow_ipp.dest_ip=192.168.1.100 49 | uci set firewall.allow_ipp.target=DNAT 50 | uci set firewall.allow_ipp.proto=tcp 51 | 52 | uci set firewall.allow_http=redirect 53 | uci set firewall.allow_http.name="Allow-HTTP-on-MFP" 54 | uci set firewall.allow_http.src=vpn 55 | uci set firewall.allow_http.src_dport=80 56 | uci set firewall.allow_http.dest=lan 57 | uci set firewall.allow_http.dest_ip=192.168.1.100 58 | uci set firewall.allow_http.target=DNAT 59 | uci set firewall.allow_http.proto=tcp 60 | 61 | uci set firewall.allow_https=redirect 62 | uci set firewall.allow_https.name="Allow-HTTPS-on-MFP" 63 | uci set firewall.allow_https.src=vpn 64 | uci set firewall.allow_https.src_dport=443 65 | uci set firewall.allow_https.dest=lan 66 | uci set firewall.allow_https.dest_ip=192.168.1.100 67 | uci set firewall.allow_https.target=DNAT 68 | uci set firewall.allow_https.proto=tcp 69 | 70 | uci set firewall.allow_jetdirect=redirect 71 | uci set firewall.allow_jetdirect.name="Allow-JetDirect-on-MFP" 72 | uci set firewall.allow_jetdirect.src=vpn 73 | uci set firewall.allow_jetdirect.src_dport=9100 74 | uci set firewall.allow_jetdirect.dest=lan 75 | uci set firewall.allow_jetdirect.dest_ip=192.168.1.100 76 | uci set firewall.allow_jetdirect.target=DNAT 77 | uci set firewall.allow_jetdirect.proto=tcp 78 | uci set firewall.allow_jetdirect.enabled=0 79 | 80 | uci set firewall.allow_snmp=redirect 81 | uci set firewall.allow_snmp.name="Allow-SNMP-on-MFP" 82 | uci set firewall.allow_snmp.src=vpn 83 | uci set firewall.allow_snmp.src_dport=161 84 | uci set firewall.allow_snmp.dest=lan 85 | uci set firewall.allow_snmp.dest_ip=192.168.1.100 86 | uci set firewall.allow_snmp.target=DNAT 87 | uci set firewall.allow_snmp.proto=udp 88 | uci set firewall.allow_snmp.enabled=0 89 | 90 | uci set firewall.allow_lpd=redirect 91 | uci set firewall.allow_lpd.name="Allow-LPD-on-MFP" 92 | uci set firewall.allow_lpd.src=vpn 93 | uci set firewall.allow_lpd.src_dport=515 94 | uci set firewall.allow_lpd.dest=lan 95 | uci set firewall.allow_lpd.dest_ip=192.168.1.100 96 | uci set firewall.allow_lpd.target=DNAT 97 | uci set firewall.allow_lpd.proto=tcp 98 | uci set firewall.allow_lpd.enabled=0 99 | 100 | /etc/init.d/dropbear disable 101 | 102 | uci set uhttpd.main.listen_http=0.0.0.0:8080 103 | 104 | EOF 105 | 106 | make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="openssl-util curl ca-certificates htop \ 107 | iftop tcpdump nmap nano mtr patch diffutils ipset usbutils luci dropbear kmod-tun netdata \ 108 | strongswan-default strongswan-mod-kernel-libipsec strongswan-mod-openssl strongswan-mod-curl strongswan-mod-ccm strongswan-mod-gcm \ 109 | -odhcpd -odhcp6c -kmod-ath9k picocom libustream-openssl kmod-crypto-gcm \ 110 | -pppd -luci-proto-ppp -kmod-ppp -ppp -ppp-mod-pppoe \ 111 | -kmod-ip6tables -ip6tables -luci-proto-ipv6 -kmod-iptunnel6 -kmod-ipsec6" 112 | 113 | -------------------------------------------------------------------------------- /certidude/builder/overlay/etc/hotplug.d/iface/50-certidude: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # To test: ACTION=ifup INTERFACE=wan sh /etc/hotplug.d/iface/50-certidude 4 | 5 | AUTHORITY=certidude.@authority[0] 6 | 7 | [ $ACTION == "ifup" ] || exit 0 8 | [ $INTERFACE == "$(uci get $AUTHORITY.trigger)" ] || exit 0 9 | 10 | /usr/bin/certidude-enroll > /var/log/certidude.log 2>&1 11 | -------------------------------------------------------------------------------- /certidude/builder/overlay/etc/profile: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | [ -f /etc/banner ] && cat /etc/banner 3 | [ -e /tmp/.failsafe ] && cat /etc/banner.failsafe 4 | 5 | export PATH=/usr/bin:/usr/sbin:/bin:/sbin 6 | export HOME=$(grep -e "^${USER:-root}:" /etc/passwd | cut -d ":" -f 6) 7 | export HOME=${HOME:-/root} 8 | export PS1='\u@\h:\w\$ ' 9 | 10 | [ -z "$KSH_VERSION" -o \! -s /etc/mkshrc ] || . /etc/mkshrc 11 | [ -x /bin/more ] || alias more=less 12 | [ -x /usr/bin/vim ] && alias vi=vim || alias vim=vi 13 | [ -x /usr/bin/arp ] || arp() { cat /proc/net/arp; } 14 | [ -x /usr/bin/ldd ] || ldd() { LD_TRACE_LOADED_OBJECTS=1 $*; } 15 | 16 | HOSTNAME=$(uci get system.@system[0].hostname) 17 | 18 | export PS1='\[\033[01;31m\]$HOSTNAME\[\033[01;34m\] \W #\[\033[00m\] ' 19 | case "$TERM" in 20 | xterm*|rxvt*) 21 | echo -ne "\033]0;${USER}@${HOSTNAME}:${PWD}\007" 22 | ;; 23 | *) 24 | ;; 25 | esac 26 | -------------------------------------------------------------------------------- /certidude/builder/overlay/etc/uci-defaults/40-hostname: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | MODEL=$(cat /etc/board.json | jsonfilter -e '@["model"]["id"]') 4 | 5 | # Hostname prefix 6 | case $MODEL in 7 | tl-*|archer-*) VENDOR=tplink ;; 8 | cf-*) VENDOR=comfast ;; 9 | *) VENDOR=ap ;; 10 | esac 11 | 12 | # Network interface with relevant MAC address 13 | case $MODEL in 14 | tl-wdr*) NIC=wlan1 ;; 15 | archer-*) NIC=eth1 ;; 16 | cf-e380ac-v2) NIC=eth0 ;; 17 | *) NIC=wlan0 ;; 18 | esac 19 | 20 | HOSTNAME=$VENDOR-$(cat /sys/class/net/$NIC/address | cut -d : -f 4- | sed -e 's/://g') 21 | uci set system.@system[0].hostname=$HOSTNAME 22 | uci set network.lan.hostname=$HOSTNAME 23 | 24 | -------------------------------------------------------------------------------- /certidude/builder/overlay/etc/uci-defaults/60-cron: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | /etc/init.d/ipsec enable 4 | 5 | # Randomize restart time 6 | OFFSET=$(awk -v min=1 -v max=59 'BEGIN{srand(); print int(min+rand()*(max-min+1))}') 7 | 8 | # wtf?! https://wiki.strongswan.org/issues/1501#note-7 9 | cat << EOF > /etc/crontabs/root 10 | #$OFFSET 2 * * * sleep 70 && touch /etc/banner && reboot 11 | $OFFSET 2 * * * ipsec restart 12 | 5 1 1 */2 * /usr/bin/certidude-enroll-renew 13 | EOF 14 | 15 | chmod 0600 /etc/crontabs/root 16 | 17 | /etc/init.d/cron enable 18 | 19 | exit 0 20 | -------------------------------------------------------------------------------- /certidude/builder/overlay/etc/uci-defaults/90-certidude-sysupgrade: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo /etc/certidude/authority/ >> /etc/sysupgrade.conf 3 | 4 | -------------------------------------------------------------------------------- /certidude/builder/overlay/etc/uci-defaults/99-uhttpd-disable-https: -------------------------------------------------------------------------------- 1 | uci delete uhttpd.main.listen_https 2 | uci delete uhttpd.main.redirect_https 3 | exit 0 4 | -------------------------------------------------------------------------------- /certidude/builder/overlay/usr/bin/certidude-enroll: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | set -x 5 | 6 | AUTHORITY=certidude.@authority[0] 7 | 8 | # TODO: iterate over all authorities 9 | 10 | GATEWAY=$(uci get $AUTHORITY.gateway) 11 | COMMON_NAME=$(uci get system.@system[0].hostname) 12 | 13 | DIR=/etc/certidude/authority/$(uci get $AUTHORITY.hostname) 14 | mkdir -p $DIR 15 | 16 | AUTHORITY_PATH=$DIR/ca_cert.pem 17 | CERTIFICATE_PATH=$DIR/host_cert.pem 18 | REQUEST_PATH=$DIR/host_req.pem 19 | KEY_PATH=$DIR/host_key.pem 20 | KEY_TYPE=$(uci get $AUTHORITY.key_type) 21 | KEY_LENGTH=$(uci get $AUTHORITY.key_length) 22 | KEY_CURVE=$(uci get $AUTHORITY.key_curve) 23 | 24 | NTP_SERVERS=$(uci get system.ntp.server) 25 | 26 | logger -t certidude -s "Fetching time from NTP servers: $NTP_SERVERS" 27 | ntpd -q -n -d -p $NTP_SERVERS 28 | 29 | logger -t certidude -s "Time is now: $(date)" 30 | 31 | # If certificate file is there assume everything's set up 32 | if [ -f $CERTIFICATE_PATH ]; then 33 | SERIAL=$(openssl x509 -in $CERTIFICATE_PATH -noout -serial | cut -d "=" -f 2 | tr [A-F] [a-f]) 34 | logger -t certidude -s "Certificate with serial $SERIAL already exists in $CERTIFICATE_PATH, attempting to bring up VPN tunnel..." 35 | exit 0 36 | fi 37 | 38 | 39 | ######################################### 40 | ### Generate private key if necessary ### 41 | ######################################### 42 | 43 | if [ ! -f $KEY_PATH ]; then 44 | case $KEY_TYPE in 45 | rsa) 46 | logger -t certidude -s "Generating $KEY_LENGTH-bit RSA key..." 47 | openssl genrsa -out $KEY_PATH.part $KEY_LENGTH 48 | openssl rsa -in $KEY_PATH.part -noout 49 | ;; 50 | ec) 51 | logger -t certidude -s "Generating $KEY_CURVE ECDSA key..." 52 | openssl ecparam -name $KEY_CURVE -genkey -noout -out $KEY_PATH.part 53 | ;; 54 | *) 55 | logger -t certidude -s "Unsupported key type $KEY_TYPE" 56 | exit 255 57 | ;; 58 | esac 59 | mv $KEY_PATH.part $KEY_PATH 60 | fi 61 | 62 | 63 | ############################ 64 | ### Fetch CA certificate ### 65 | ############################ 66 | 67 | if [ ! -f $AUTHORITY_PATH ]; then 68 | 69 | logger -t certidude -s "Fetching CA certificate from $URL/api/certificate/" 70 | curl -f -s http://$(uci get $AUTHORITY.hostname)/api/certificate/ -o $AUTHORITY_PATH.part 71 | if [ $? -ne 0 ]; then 72 | logger -t certidude -s "Failed to receive CA certificate, server responded: $(cat $AUTHORITY_PATH.part)" 73 | exit 10 74 | fi 75 | 76 | openssl x509 -in $AUTHORITY_PATH.part -noout 77 | if [ $? -ne 0 ]; then 78 | logger -t certidude -s "Received invalid CA certificate" 79 | exit 11 80 | fi 81 | 82 | mv $AUTHORITY_PATH.part $AUTHORITY_PATH 83 | fi 84 | 85 | logger -t certidude -s "CA certificate md5sum: $(md5sum -b $AUTHORITY_PATH)" 86 | 87 | 88 | ##################################### 89 | ### Generate request if necessary ### 90 | ##################################### 91 | 92 | if [ ! -f $REQUEST_PATH ]; then 93 | openssl req -new -sha256 -key $KEY_PATH -out $REQUEST_PATH.part -subj "/CN=$COMMON_NAME" 94 | mv $REQUEST_PATH.part $REQUEST_PATH 95 | fi 96 | 97 | logger -t certidude -s "Request md5sum is $(md5sum -b $REQUEST_PATH)" 98 | 99 | curl --cert-status -f -L \ 100 | -H "Content-Type: application/pkcs10" \ 101 | --cacert $AUTHORITY_PATH \ 102 | --data-binary @$REQUEST_PATH \ 103 | https://$(uci get $AUTHORITY.hostname):8443/api/request/?autosign=true\&wait=yes -o $CERTIFICATE_PATH.part 104 | 105 | # TODO: Loop until we get exitcode 0 106 | # TODO: Use backoff time $((2\*X)) 107 | 108 | if [ $? -ne 0 ]; then 109 | echo "Failed to fetch certificate" 110 | exit 21 111 | fi 112 | 113 | # Verify certificate 114 | openssl verify -CAfile $AUTHORITY_PATH $CERTIFICATE_PATH.part 115 | 116 | if [ $? -ne 0 ]; then 117 | logger -t certidude -s "Received bogus certificate!" 118 | exit 22 119 | fi 120 | 121 | logger -t certidude -s "Certificate md5sum: $(md5sum -b $CERTIFICATE_PATH.part)" 122 | 123 | uci commit 124 | 125 | mv $CERTIFICATE_PATH.part $CERTIFICATE_PATH 126 | 127 | # Start services 128 | logger -t certidude -s "Starting IPSec IKEv2 daemon..." 129 | /etc/init.d/ipsec restart 130 | -------------------------------------------------------------------------------- /certidude/builder/overlay/usr/bin/certidude-enroll-renew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | AUTHORITY=certidude.@authority[0] 4 | URL=https://$(uci get $AUTHORITY.hostname):8443 5 | DIR=/etc/certidude/authority/$(uci get $AUTHORITY.hostname) 6 | AUTHORITY_PATH=$DIR/ca_cert.pem 7 | CERTIFICATE_PATH=$DIR/host_cert.pem 8 | REQUEST_PATH=$DIR/host_req.pem 9 | KEY_PATH=$DIR/host_key.pem 10 | 11 | # TODO: fix Accepted 202 here 12 | 13 | curl --cert-status -f -L \ 14 | -H "Content-Type: application/pkcs10" \ 15 | --data-binary @$REQUEST_PATH \ 16 | --cacert $AUTHORITY_PATH \ 17 | --key $KEY_PATH \ 18 | --cert $CERTIFICATE_PATH \ 19 | $URL/api/request/ -o $CERTIFICATE_PATH.part 20 | 21 | if [ $? -eq 0 ]; then 22 | logger -t certidude -s "Certificate renewal successful" 23 | mv $CERTIFICATE_PATH.part $CERTIFICATE_PATH 24 | ipsec reload 25 | else 26 | logger -t certidude -s "Failed to renew certificate" 27 | fi 28 | -------------------------------------------------------------------------------- /certidude/common.py: -------------------------------------------------------------------------------- 1 | 2 | import os 3 | import click 4 | import subprocess 5 | from setproctitle import getproctitle 6 | from random import SystemRandom 7 | 8 | random = SystemRandom() 9 | 10 | try: 11 | from time import time_ns 12 | except ImportError: 13 | from time import time 14 | def time_ns(): 15 | return int(time() * 10**9) # 64 bits integer, 32 ns bits 16 | 17 | MAPPING = dict( 18 | common_name="CN", 19 | organizational_unit_name="OU", 20 | organization_name="O", 21 | domain_component="DC" 22 | ) 23 | 24 | def cert_to_dn(cert): 25 | d = [] 26 | for key, value in cert["tbs_certificate"]["subject"].native.items(): 27 | if not isinstance(value, list): 28 | value = [value] 29 | for comp in value: 30 | d.append("%s=%s" % (MAPPING[key], comp)) 31 | return ", ".join(d) 32 | 33 | def cn_to_dn(common_name, namespace, o=None, ou=None): 34 | from asn1crypto.x509 import Name, RelativeDistinguishedName, NameType, DirectoryString, RDNSequence, NameTypeAndValue, UTF8String, DNSName 35 | 36 | rdns = [] 37 | 38 | for dc in reversed(namespace.split(".")): 39 | rdns.append(RelativeDistinguishedName([ 40 | NameTypeAndValue({ 41 | 'type': NameType.map("domain_component"), 42 | 'value': DNSName(value=dc) 43 | }) 44 | ])) 45 | 46 | if o: 47 | rdns.append(RelativeDistinguishedName([ 48 | NameTypeAndValue({ 49 | 'type': NameType.map("organization_name"), 50 | 'value': DirectoryString( 51 | name="utf8_string", 52 | value=UTF8String(o)) 53 | }) 54 | ])) 55 | 56 | if ou: 57 | rdns.append(RelativeDistinguishedName([ 58 | NameTypeAndValue({ 59 | 'type': NameType.map("organizational_unit_name"), 60 | 'value': DirectoryString( 61 | name="utf8_string", 62 | value=UTF8String(ou)) 63 | }) 64 | ])) 65 | 66 | rdns.append(RelativeDistinguishedName([ 67 | NameTypeAndValue({ 68 | 'type': NameType.map("common_name"), 69 | 'value': DirectoryString( 70 | name="utf8_string", 71 | value=UTF8String(common_name)) 72 | }) 73 | ])) 74 | 75 | return Name(name='', value=RDNSequence(rdns)) 76 | 77 | def selinux_fixup(path): 78 | """ 79 | Fix OpenVPN credential store security context on Fedora 80 | """ 81 | if os.path.exists("/usr/bin/chcon"): 82 | cmd = "chcon", "--type=home_cert_t", path 83 | subprocess.call(cmd) 84 | 85 | def drop_privileges(): 86 | from certidude import config 87 | import pwd 88 | _, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude") 89 | restricted_groups = [] 90 | restricted_groups.append(gid) 91 | 92 | # PAM needs access to /etc/shadow 93 | if config.AUTHENTICATION_BACKENDS == {"pam"}: 94 | import grp 95 | name, passwd, num, mem = grp.getgrnam("shadow") 96 | click.echo("Adding current user to shadow group due to PAM authentication backend") 97 | restricted_groups.append(num) 98 | 99 | os.setgroups(restricted_groups) 100 | os.setgid(gid) 101 | os.setuid(uid) 102 | click.echo("Switched %s (pid=%d) to user %s (uid=%d, gid=%d); member of groups %s" % 103 | (getproctitle(), os.getpid(), "certidude", os.getuid(), os.getgid(), ", ".join([str(j) for j in os.getgroups()]))) 104 | os.umask(0o007) 105 | 106 | def apt(packages): 107 | """ 108 | Install packages for Debian and Ubuntu 109 | """ 110 | if os.path.exists("/usr/bin/apt-get"): 111 | cmd = ["/usr/bin/apt-get", "install", "-yqq", "-o", "Dpkg::Options::=--force-confold"] + packages.split(" ") 112 | click.echo("Running: %s" % " ".join(cmd)) 113 | subprocess.call(cmd) 114 | return True 115 | return False 116 | 117 | 118 | def rpm(packages): 119 | """ 120 | Install packages for Fedora and CentOS 121 | """ 122 | if os.path.exists("/usr/bin/dnf"): 123 | cmd = ["/usr/bin/dnf", "install", "-y"] + packages.split(" ") 124 | click.echo("Running: %s" % " ".join(cmd)) 125 | subprocess.call(cmd) 126 | return True 127 | return False 128 | 129 | 130 | def pip(packages): 131 | click.echo("Running: pip3 install %s" % packages) 132 | import pip 133 | pip.main(['install'] + packages.split(" ")) 134 | return True 135 | 136 | def generate_serial(): 137 | return time_ns() << 56 | random.randint(0, 2**56-1) 138 | 139 | -------------------------------------------------------------------------------- /certidude/config.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | import ipaddress 3 | import os 4 | from certidude import const 5 | from certidude.profile import SignatureProfile 6 | from collections import OrderedDict 7 | from datetime import timedelta 8 | 9 | # Options that are parsed from config file are fetched here 10 | 11 | cp = configparser.RawConfigParser() 12 | cp.readfp(open(const.SERVER_CONFIG_PATH, "r")) 13 | 14 | AUTHENTICATION_BACKENDS = set([j for j in 15 | cp.get("authentication", "backends").split(" ") if j]) # kerberos, pam, ldap 16 | AUTHORIZATION_BACKEND = cp.get("authorization", "backend") # whitelist, ldap, posix 17 | ACCOUNTS_BACKEND = cp.get("accounts", "backend") # posix, ldap 18 | MAIL_SUFFIX = cp.get("accounts", "mail suffix") 19 | 20 | KERBEROS_KEYTAB = cp.get("authentication", "kerberos keytab") 21 | KERBEROS_REALM = cp.get("authentication", "kerberos realm") 22 | LDAP_AUTHENTICATION_URI = cp.get("authentication", "ldap uri") 23 | LDAP_GSSAPI_CRED_CACHE = cp.get("accounts", "ldap gssapi credential cache") 24 | LDAP_ACCOUNTS_URI = cp.get("accounts", "ldap uri") 25 | LDAP_BASE = cp.get("accounts", "ldap base") 26 | LDAP_MAIL_ATTRIBUTE = cp.get("accounts", "ldap mail attribute") 27 | 28 | USER_SUBNETS = set([ipaddress.ip_network(j) for j in 29 | cp.get("authorization", "user subnets").replace(",", " ").split(" ") if j]) 30 | ADMIN_SUBNETS = set([ipaddress.ip_network(j) for j in 31 | cp.get("authorization", "admin subnets").replace(",", " ").split(" ") if j]) 32 | AUTOSIGN_SUBNETS = set([ipaddress.ip_network(j) for j in 33 | cp.get("authorization", "autosign subnets").replace(",", " ").split(" ") if j]) 34 | REQUEST_SUBNETS = set([ipaddress.ip_network(j) for j in 35 | cp.get("authorization", "request subnets").replace(",", " ").split(" ") if j]).union(AUTOSIGN_SUBNETS) 36 | SCEP_SUBNETS = set([ipaddress.ip_network(j) for j in 37 | cp.get("authorization", "scep subnets").replace(",", " ").split(" ") if j]) 38 | OCSP_SUBNETS = set([ipaddress.ip_network(j) for j in 39 | cp.get("authorization", "ocsp subnets").replace(",", " ").split(" ") if j]) 40 | CRL_SUBNETS = set([ipaddress.ip_network(j) for j in 41 | cp.get("authorization", "crl subnets").replace(",", " ").split(" ") if j]) 42 | RENEWAL_SUBNETS = set([ipaddress.ip_network(j) for j in 43 | cp.get("authorization", "renewal subnets").replace(",", " ").split(" ") if j]) 44 | OVERWRITE_SUBNETS = set([ipaddress.ip_network(j) for j in 45 | cp.get("authorization", "overwrite subnets").replace(",", " ").split(" ") if j]) 46 | MACHINE_ENROLLMENT_SUBNETS = set([ipaddress.ip_network(j) for j in 47 | cp.get("authorization", "machine enrollment subnets").replace(",", " ").split(" ") if j]) 48 | KERBEROS_SUBNETS = set([ipaddress.ip_network(j) for j in 49 | cp.get("authorization", "kerberos subnets").replace(",", " ").split(" ") if j]) 50 | 51 | AUTHORITY_DIR = "/var/lib/certidude" 52 | AUTHORITY_PRIVATE_KEY_PATH = cp.get("authority", "private key path") 53 | AUTHORITY_CERTIFICATE_PATH = cp.get("authority", "certificate path") 54 | SELF_KEY_PATH = cp.get("authority", "self key path") 55 | REQUESTS_DIR = cp.get("authority", "requests dir") 56 | SIGNED_DIR = cp.get("authority", "signed dir") 57 | SIGNED_BY_SERIAL_DIR = os.path.join(SIGNED_DIR, "by-serial") 58 | REVOKED_DIR = cp.get("authority", "revoked dir") 59 | EXPIRED_DIR = cp.get("authority", "expired dir") 60 | 61 | MAILER_NAME = cp.get("mailer", "name") 62 | MAILER_ADDRESS = cp.get("mailer", "address") 63 | 64 | BOOTSTRAP_TEMPLATE = cp.get("bootstrap", "services template") 65 | 66 | USER_ENROLLMENT_ALLOWED = { 67 | "forbidden": False, "single allowed": True, "multiple allowed": True }[ 68 | cp.get("authority", "user enrollment")] 69 | USER_MULTIPLE_CERTIFICATES = { 70 | "forbidden": False, "single allowed": False, "multiple allowed": True }[ 71 | cp.get("authority", "user enrollment")] 72 | 73 | REQUEST_SUBMISSION_ALLOWED = cp.getboolean("authority", "request submission allowed") 74 | AUTHORITY_CERTIFICATE_URL = cp.get("signature", "authority certificate url") 75 | AUTHORITY_CRL_URL = "http://%s/api/revoked" % const.FQDN 76 | 77 | REVOCATION_LIST_LIFETIME = cp.getint("signature", "revocation list lifetime") 78 | 79 | EVENT_SOURCE_TOKEN = cp.get("push", "event source token") 80 | EVENT_SOURCE_PUBLISH = cp.get("push", "event source publish") 81 | EVENT_SOURCE_SUBSCRIBE = cp.get("push", "event source subscribe") 82 | LONG_POLL_PUBLISH = cp.get("push", "long poll publish") 83 | LONG_POLL_SUBSCRIBE = cp.get("push", "long poll subscribe") 84 | 85 | LOGGING_BACKEND = cp.get("logging", "backend") 86 | 87 | USERS_GROUP = cp.get("authorization", "posix user group") 88 | ADMIN_GROUP = cp.get("authorization", "posix admin group") 89 | LDAP_USER_FILTER = cp.get("authorization", "ldap user filter") 90 | LDAP_ADMIN_FILTER = cp.get("authorization", "ldap admin filter") 91 | LDAP_COMPUTER_FILTER = cp.get("authorization", "ldap computer filter") 92 | 93 | if "%s" not in LDAP_USER_FILTER: raise ValueError("No placeholder %s for username in 'ldap user filter'") 94 | if "%s" not in LDAP_ADMIN_FILTER: raise ValueError("No placeholder %s for username in 'ldap admin filter'") 95 | 96 | TAG_TYPES = [j.split("/", 1) + [cp.get("tagging", j)] for j in cp.options("tagging")] 97 | 98 | # Tokens 99 | TOKEN_URL = cp.get("token", "url") 100 | TOKEN_BACKEND = cp.get("token", "backend") 101 | TOKEN_LIFETIME = timedelta(minutes=cp.getint("token", "lifetime")) # Convert minutes to seconds 102 | TOKEN_DATABASE = cp.get("token", "database") 103 | TOKEN_OVERWRITE_PERMITTED = cp.getboolean("token", "overwrite permitted") 104 | # TODO: Check if we don't have base or servers 105 | 106 | # The API call for looking up scripts uses following directory as root 107 | SCRIPT_DIR = cp.get("script", "path") 108 | 109 | from configparser import ConfigParser 110 | profile_config = ConfigParser() 111 | profile_config.readfp(open(const.PROFILE_CONFIG_PATH)) 112 | 113 | PROFILES = dict([(key, SignatureProfile(key, 114 | profile_config.get(key, "title"), 115 | profile_config.get(key, "ou"), 116 | profile_config.getboolean(key, "ca"), 117 | profile_config.getint(key, "lifetime"), 118 | profile_config.get(key, "key usage"), 119 | profile_config.get(key, "extended key usage"), 120 | profile_config.get(key, "common name"), 121 | profile_config.get(key, "revoked url"), 122 | profile_config.get(key, "responder url") 123 | )) for key in profile_config.sections() if profile_config.getboolean(key, "enabled")]) 124 | 125 | cp2 = configparser.RawConfigParser() 126 | cp2.readfp(open(const.BUILDER_CONFIG_PATH, "r")) 127 | IMAGE_BUILDER_PROFILES = [(j, cp2.get(j, "title"), cp2.get(j, "rename")) for j in cp2.sections() if cp2.getboolean(j, "enabled")] 128 | 129 | SERVICE_PROTOCOLS = set([j.lower() for j in cp.get("service", "protocols").split(" ") if j]) 130 | SERVICE_ROUTERS = cp.get("service", "routers") 131 | -------------------------------------------------------------------------------- /certidude/const.py: -------------------------------------------------------------------------------- 1 | 2 | import click 3 | import os 4 | import socket 5 | import sys 6 | from datetime import timedelta 7 | 8 | KEY_SIZE = 1024 if os.getenv("COVERAGE_PROCESS_START") else 4096 9 | CURVE_NAME = "secp384r1" 10 | 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])?$" 11 | RE_HOSTNAME = "^[a-z0-9]([a-z0-9\-_]{0,61}[a-z0-9])?$" 12 | RE_COMMON_NAME = "^[A-Za-z0-9\-\.\_@]+$" 13 | CLOCK_SKEW_TOLERANCE = timedelta(minutes=5) # Kerberos-like clock skew tolerance 14 | 15 | RUN_DIR = "/run/certidude" 16 | CONFIG_DIR = "/etc/certidude" 17 | SERVER_CONFIG_PATH = os.path.join(CONFIG_DIR, "server.conf") 18 | BUILDER_CONFIG_PATH = os.path.join(CONFIG_DIR, "builder.conf") 19 | SCRIPT_DIR = os.path.join(CONFIG_DIR, "script") 20 | BUILDER_SITE_SCRIPT = os.path.join(SCRIPT_DIR, "site.sh") 21 | PROFILE_CONFIG_PATH = os.path.join(CONFIG_DIR, "profile.conf") 22 | CLIENT_CONFIG_PATH = os.path.join(CONFIG_DIR, "client.conf") 23 | SERVICES_CONFIG_PATH = os.path.join(CONFIG_DIR, "services.conf") 24 | SERVER_PID_PATH = os.path.join(RUN_DIR, "server.pid") 25 | STORAGE_PATH = "/var/lib/certidude/" 26 | 27 | try: 28 | FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3] 29 | except socket.gaierror: 30 | FQDN = socket.gethostname() 31 | if hasattr(FQDN, "decode"): # Keep client backwards compatible with Python 2.x 32 | FQDN = FQDN.decode("ascii") 33 | 34 | try: 35 | HOSTNAME, DOMAIN = FQDN.split(".", 1) 36 | except ValueError: # If FQDN is not configured 37 | HOSTNAME = FQDN 38 | DOMAIN = None 39 | 40 | # TODO: lazier, otherwise gets evaluated before installing package 41 | if os.path.exists("/etc/strongswan/ipsec.conf"): # fedora dafuq?! 42 | STRONGSWAN_PREFIX = "/etc/strongswan" 43 | else: 44 | STRONGSWAN_PREFIX = "/etc" 45 | -------------------------------------------------------------------------------- /certidude/decorators.py: -------------------------------------------------------------------------------- 1 | import click 2 | import ipaddress 3 | import json 4 | import logging 5 | import os 6 | import types 7 | from datetime import date, time, datetime, timedelta 8 | from urllib.parse import urlparse 9 | 10 | logger = logging.getLogger("api") 11 | 12 | def csrf_protection(func): 13 | """ 14 | Protect resource from common CSRF attacks by checking user agent and referrer 15 | """ 16 | import falcon 17 | def wrapped(self, req, resp, *args, **kwargs): 18 | # Assume curl and python-requests are used intentionally 19 | if req.user_agent.startswith("curl/") or req.user_agent.startswith("python-requests/"): 20 | return func(self, req, resp, *args, **kwargs) 21 | 22 | # For everything else assert referrer 23 | referrer = req.headers.get("REFERER") 24 | 25 | 26 | if referrer: 27 | scheme, netloc, path, params, query, fragment = urlparse(referrer) 28 | if ":" in netloc: 29 | host, port = netloc.split(":", 1) 30 | else: 31 | host, port = netloc, None 32 | if host == req.host: 33 | return func(self, req, resp, *args, **kwargs) 34 | 35 | # Kaboom! 36 | logger.warning("Prevented clickbait from '%s' with user agent '%s'", 37 | referrer or "-", req.user_agent) 38 | raise falcon.HTTPForbidden("Forbidden", 39 | "No suitable UA or referrer provided, cross-site scripting disabled") 40 | return wrapped 41 | 42 | 43 | class MyEncoder(json.JSONEncoder): 44 | def default(self, obj): 45 | from certidude.user import User 46 | if isinstance(obj, ipaddress._IPAddressBase): 47 | return str(obj) 48 | if isinstance(obj, set): 49 | return tuple(obj) 50 | if isinstance(obj, datetime): 51 | return obj.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" 52 | if isinstance(obj, date): 53 | return obj.strftime("%Y-%m-%d") 54 | if isinstance(obj, timedelta): 55 | return obj.total_seconds() 56 | if isinstance(obj, types.GeneratorType): 57 | return tuple(obj) 58 | if isinstance(obj, User): 59 | return dict(name=obj.name, given_name=obj.given_name, 60 | surname=obj.surname, mail=obj.mail) 61 | return json.JSONEncoder.default(self, obj) 62 | 63 | 64 | def serialize(func): 65 | """ 66 | Falcon response serialization 67 | """ 68 | import falcon 69 | def wrapped(instance, req, resp, **kwargs): 70 | retval = func(instance, req, resp, **kwargs) 71 | if not resp.body and not resp.location: 72 | if not req.client_accepts("application/json"): 73 | logger.debug("Client did not accept application/json") 74 | raise falcon.HTTPUnsupportedMediaType( 75 | "Client did not accept application/json") 76 | resp.set_header("Cache-Control", "no-cache, no-store, must-revalidate") 77 | resp.set_header("Pragma", "no-cache") 78 | resp.set_header("Expires", "0") 79 | resp.body = json.dumps(retval, cls=MyEncoder) 80 | return wrapped 81 | 82 | -------------------------------------------------------------------------------- /certidude/errors.py: -------------------------------------------------------------------------------- 1 | 2 | class RequestExists(Exception): 3 | pass 4 | 5 | class RequestDoesNotExist(Exception): 6 | pass 7 | 8 | class FatalError(Exception): 9 | """ 10 | Exception to be raised when user intervention is required 11 | """ 12 | pass 13 | 14 | class DuplicateCommonNameError(FatalError): 15 | pass 16 | -------------------------------------------------------------------------------- /certidude/mailer.py: -------------------------------------------------------------------------------- 1 | 2 | import click 3 | import os 4 | import smtplib 5 | from certidude.user import User 6 | from markdown import markdown 7 | from jinja2 import Environment, PackageLoader 8 | from email.mime.multipart import MIMEMultipart 9 | from email.mime.text import MIMEText 10 | from email.mime.base import MIMEBase 11 | from email.header import Header 12 | from urllib.parse import urlparse 13 | 14 | env = Environment(loader=PackageLoader("certidude", "templates/mail")) 15 | 16 | def send(template, to=None, secondary=None, include_admins=True, attachments=(), **context): 17 | from certidude import authority, config 18 | 19 | recipients = () 20 | if include_admins: 21 | recipients = tuple(User.objects.filter_admins()) 22 | if to: 23 | recipients = (to,) + recipients 24 | if secondary: 25 | recipients = (secondary,) + recipients 26 | 27 | 28 | click.echo("Sending e-mail %s to %s" % (template, recipients)) 29 | 30 | subject, text = env.get_template(template).render(context).split("\n\n", 1) 31 | html = markdown(text) 32 | 33 | msg = MIMEMultipart("alternative") 34 | msg["Subject"] = Header(subject) 35 | msg["From"] = Header(config.MAILER_NAME) 36 | msg["From"].append("<%s>" % config.MAILER_ADDRESS) 37 | 38 | msg["To"] = Header() 39 | for user in recipients: 40 | if isinstance(user, User): 41 | full_name, user = user.format() 42 | if full_name: 43 | msg["To"].append(full_name) 44 | msg["To"].append(user) 45 | msg["To"].append(", ") 46 | 47 | part1 = MIMEText(text, "plain", "utf-8") 48 | part2 = MIMEText(html, "html", "utf-8") 49 | 50 | msg.attach(part1) 51 | msg.attach(part2) 52 | 53 | for attachment, content_type, suggested_filename in attachments: 54 | part = MIMEBase(*content_type.split("/")) 55 | part.add_header('Content-Disposition', 'attachment', filename=suggested_filename) 56 | part.set_payload(attachment) 57 | msg.attach(part) 58 | 59 | if config.MAILER_ADDRESS: 60 | click.echo("Sending to: %s" % msg["to"]) 61 | conn = smtplib.SMTP("localhost") 62 | conn.sendmail(config.MAILER_ADDRESS, [u.mail if isinstance(u, User) else u for u in recipients], msg.as_string()) 63 | -------------------------------------------------------------------------------- /certidude/mysqllog.py: -------------------------------------------------------------------------------- 1 | 2 | import logging 3 | import time 4 | from datetime import datetime 5 | from certidude.relational import RelationalMixin 6 | 7 | class LogHandler(logging.Handler, RelationalMixin): 8 | SQL_CREATE_TABLES = "log_tables.sql" 9 | 10 | def __init__(self, uri): 11 | logging.Handler.__init__(self) 12 | RelationalMixin.__init__(self, uri) 13 | 14 | def emit(self, record): 15 | self.sql_execute("log_insert_entry.sql", 16 | datetime.utcfromtimestamp(record.created), 17 | record.name, 18 | record.levelno, 19 | record.levelname.lower(), 20 | record.msg % record.args, record.module, 21 | record.funcName, 22 | record.lineno, 23 | logging._defaultFormatter.formatException(record.exc_info) if record.exc_info else "", 24 | record.process, 25 | record.thread, 26 | record.threadName) 27 | -------------------------------------------------------------------------------- /certidude/profile.py: -------------------------------------------------------------------------------- 1 | 2 | import click 3 | from datetime import timedelta 4 | from certidude import const 5 | 6 | class SignatureProfile(object): 7 | def __init__(self, slug, title, ou, ca, lifetime, key_usage, extended_key_usage, common_name, revoked_url, responder_url): 8 | self.slug = slug 9 | self.title = title 10 | self.ou = ou or None 11 | self.ca = ca 12 | self.lifetime = lifetime 13 | self.key_usage = set(key_usage.split(" ")) if key_usage else set() 14 | self.extended_key_usage = set(extended_key_usage.split(" ")) if extended_key_usage else set() 15 | self.responder_url = responder_url 16 | self.revoked_url = revoked_url 17 | 18 | if common_name.startswith("^"): 19 | self.common_name = common_name 20 | elif common_name == "RE_HOSTNAME": 21 | self.common_name = const.RE_HOSTNAME 22 | elif common_name == "RE_FQDN": 23 | self.common_name = const.RE_FQDN 24 | elif common_name == "RE_COMMON_NAME": 25 | self.common_name = const.RE_COMMON_NAME 26 | else: 27 | raise ValueError("Invalid common name constraint %s" % common_name) 28 | 29 | @classmethod 30 | def from_cert(self, cert): 31 | """ 32 | Derive signature profile from an already signed certificate, eg for renewal 33 | """ 34 | lifetime = cert["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None) - \ 35 | cert["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None) 36 | return SignatureProfile( 37 | None, "Renewal", cert.subject.native.get("organizational_unit_name"), 38 | cert.ca, lifetime.days, 39 | " ".join(cert.key_usage_value.native), 40 | " ".join(cert.extended_key_usage_value.native), "^") 41 | 42 | 43 | def serialize(self): 44 | return dict([(key, getattr(self,key)) for key in ( 45 | "slug", "title", "ou", "ca", "lifetime", "key_usage", "extended_key_usage", "common_name", "responder_url", "revoked_url")]) 46 | 47 | def __repr__(self): 48 | bits = [] 49 | if self.lifetime >= 365: 50 | bits.append("%d years" % (self.lifetime / 365)) 51 | if self.lifetime % 365: 52 | bits.append("%d days" % (self.lifetime % 365)) 53 | 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)" % ( 54 | self.slug, self.title, self.ca, self.ou, " ".join(bits), 55 | self.key_usage, self.extended_key_usage, 56 | repr(self.common_name), 57 | repr(self.responder_url), 58 | repr(self.revoked_url)) 59 | 60 | -------------------------------------------------------------------------------- /certidude/push.py: -------------------------------------------------------------------------------- 1 | 2 | import click 3 | import json 4 | import logging 5 | import requests 6 | from datetime import datetime 7 | from certidude import config 8 | 9 | 10 | def publish(event_type, event_data=''): 11 | """ 12 | Publish event on nchan EventSource publisher 13 | """ 14 | assert event_type, "No event type specified" 15 | 16 | if not isinstance(event_data, str): 17 | from certidude.decorators import MyEncoder 18 | event_data = json.dumps(event_data, cls=MyEncoder) 19 | 20 | url = config.EVENT_SOURCE_PUBLISH % config.EVENT_SOURCE_TOKEN 21 | click.echo("Publishing %s event '%s' on %s" % (event_type, event_data, url)) 22 | 23 | try: 24 | notification = requests.post( 25 | url, 26 | data=event_data, 27 | headers={"X-EventSource-Event": event_type, "User-Agent": "Certidude API"}) 28 | if notification.status_code == requests.codes.created: 29 | pass # Sent to client 30 | elif notification.status_code == requests.codes.accepted: 31 | pass # Buffered in nchan 32 | else: 33 | click.echo("Failed to submit event to push server, server responded %d" % ( 34 | notification.status_code)) 35 | except requests.exceptions.ConnectionError: 36 | click.echo("Failed to submit event to push server, connection error") 37 | 38 | 39 | class EventSourceLogHandler(logging.Handler): 40 | """ 41 | To be used with Python log handling framework for publishing log entries 42 | """ 43 | def emit(self, record): 44 | publish("log-entry", dict( 45 | created = datetime.utcfromtimestamp(record.created), 46 | message = record.msg % record.args, 47 | severity = record.levelname.lower())) 48 | 49 | -------------------------------------------------------------------------------- /certidude/relational.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import click 4 | import re 5 | import os 6 | from urllib.parse import urlparse 7 | 8 | SCRIPTS = {} 9 | 10 | class RelationalMixin(object): 11 | """ 12 | Thin wrapper around SQLite and MySQL database connectors 13 | """ 14 | 15 | SQL_CREATE_TABLES = "" 16 | 17 | class DoesNotExist(Exception): 18 | pass 19 | 20 | def __init__(self, uri): 21 | self.uri = urlparse(uri) 22 | 23 | def sql_connect(self): 24 | if self.uri.scheme == "mysql": 25 | import mysql.connector 26 | conn = mysql.connector.connect( 27 | user=self.uri.username, 28 | password=self.uri.password, 29 | host=self.uri.hostname, 30 | database=self.uri.path[1:]) 31 | elif self.uri.scheme == "sqlite": 32 | if self.uri.netloc: 33 | raise ValueError("Malformed database URI %s" % self.uri) 34 | import sqlite3 35 | conn = sqlite3.connect(self.uri.path, 36 | detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES) 37 | else: 38 | raise NotImplementedError("Unsupported database scheme %s, currently only mysql://user:pass@host/database or sqlite:///path/to/database.sqlite is supported" % o.scheme) 39 | 40 | if self.SQL_CREATE_TABLES and self.SQL_CREATE_TABLES not in SCRIPTS: 41 | cur = conn.cursor() 42 | buf, path = self.sql_load(self.SQL_CREATE_TABLES) 43 | click.echo("Executing: %s" % path) 44 | if self.uri.scheme == "sqlite": 45 | cur.executescript(buf) 46 | else: 47 | cur.execute(buf, multi=True) 48 | conn.commit() 49 | cur.close() 50 | return conn 51 | 52 | def sql_resolve_script(self, filename): 53 | return os.path.realpath(os.path.join(os.path.dirname(__file__), 54 | "sql", self.uri.scheme, filename)) 55 | 56 | 57 | def sql_load(self, filename): 58 | if filename in SCRIPTS: 59 | return SCRIPTS[filename] 60 | 61 | fh = open(self.sql_resolve_script(filename)) 62 | click.echo("Caching SQL script: %s" % fh.name) 63 | buf = re.sub("\s*\n\s*", " ", fh.read()) 64 | SCRIPTS[filename] = buf, fh.name 65 | fh.close() 66 | return buf, fh.name 67 | 68 | 69 | def sql_execute(self, script, *args): 70 | conn = self.sql_connect() 71 | cursor = conn.cursor() 72 | click.echo("Executing %s with %s" % (script, args)) 73 | buf, path = self.sql_load(script) 74 | cursor.execute(buf, args) 75 | rowid = cursor.lastrowid 76 | conn.commit() 77 | cursor.close() 78 | conn.close() 79 | return rowid 80 | 81 | def iterfetch(self, query, *args): 82 | conn = self.sql_connect() 83 | cursor = conn.cursor() 84 | cursor.execute(query, args) 85 | cols = [j[0] for j in cursor.description] 86 | def g(): 87 | for row in cursor: 88 | yield dict(zip(cols, row)) 89 | cursor.close() 90 | conn.close() 91 | return tuple(g()) 92 | 93 | def get(self, query, *args): 94 | conn = self.sql_connect() 95 | cursor = conn.cursor() 96 | cursor.execute(query, args) 97 | row = cursor.fetchone() 98 | cursor.close() 99 | conn.close() 100 | if not row: 101 | raise self.DoesNotExist("No matches for query '%s' with parameters %s" % (query, repr(args))) 102 | return row 103 | 104 | def execute(self, query, *args): 105 | conn = self.sql_connect() 106 | cursor = conn.cursor() 107 | cursor.execute(query, args) 108 | affected_rows = cursor.rowcount 109 | cursor.close() 110 | conn.commit() 111 | conn.close() 112 | return affected_rows 113 | -------------------------------------------------------------------------------- /certidude/sql/mysql/log_insert_entry.sql: -------------------------------------------------------------------------------- 1 | insert into log ( 2 | created, 3 | facility, 4 | level, 5 | severity, 6 | message, 7 | module, 8 | func, 9 | lineno, 10 | exception, 11 | process, 12 | thread, 13 | thread_name 14 | ) values ( 15 | %s, 16 | %s, 17 | %s, 18 | %s, 19 | %s, 20 | %s, 21 | %s, 22 | %s, 23 | %s, 24 | %s, 25 | %s, 26 | %s 27 | ); 28 | -------------------------------------------------------------------------------- /certidude/sql/mysql/log_tables.sql: -------------------------------------------------------------------------------- 1 | create table if not exists log ( 2 | created datetime, 3 | facility varchar(30), 4 | level int, 5 | severity varchar(10), 6 | message text, 7 | module varchar(20), 8 | func varchar(50), 9 | lineno int, 10 | exception text, 11 | process int, 12 | thread text, 13 | thread_name text 14 | ) 15 | -------------------------------------------------------------------------------- /certidude/sql/sqlite/log_insert_entry.sql: -------------------------------------------------------------------------------- 1 | insert into log ( 2 | created, 3 | facility, 4 | level, 5 | severity, 6 | message, 7 | module, 8 | func, 9 | lineno, 10 | exception, 11 | process, 12 | thread, 13 | thread_name 14 | ) values ( 15 | ?, 16 | ?, 17 | ?, 18 | ?, 19 | ?, 20 | ?, 21 | ?, 22 | ?, 23 | ?, 24 | ?, 25 | ?, 26 | ? 27 | ); 28 | -------------------------------------------------------------------------------- /certidude/sql/sqlite/log_tables.sql: -------------------------------------------------------------------------------- 1 | create table if not exists log ( 2 | id integer primary key autoincrement, 3 | created datetime, 4 | facility varchar(30), 5 | level int, 6 | severity varchar(10), 7 | message text, 8 | module varchar(20), 9 | func varchar(20), 10 | lineno int, 11 | exception text, 12 | process int, 13 | thread text, 14 | thread_name text 15 | ) 16 | -------------------------------------------------------------------------------- /certidude/sql/sqlite/token_issue.sql: -------------------------------------------------------------------------------- 1 | insert into token ( 2 | created, 3 | expires, 4 | uuid, 5 | issuer, 6 | subject, 7 | mail, 8 | profile 9 | ) values ( 10 | ?, 11 | ?, 12 | ?, 13 | ?, 14 | ?, 15 | ?, 16 | ? 17 | ); 18 | -------------------------------------------------------------------------------- /certidude/sql/sqlite/token_tables.sql: -------------------------------------------------------------------------------- 1 | create table if not exists token ( 2 | id integer primary key autoincrement, 3 | created datetime, 4 | used datetime, 5 | expires datetime, 6 | uuid char(32), 7 | issuer char(30), 8 | subject varchar(30), 9 | mail varchar(128), 10 | profile varchar(10), 11 | 12 | constraint unique_uuid unique(uuid) 13 | ) 14 | -------------------------------------------------------------------------------- /certidude/static/502.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "502 Bad Gateway", 3 | "description": "It seems the server had bit of a hiccup, perhaps this helps: systemctl restart certidude-backend && journalctl -f" 4 | } 5 | -------------------------------------------------------------------------------- /certidude/static/css/style.css: -------------------------------------------------------------------------------- 1 | 2 | @keyframes fresh { 3 | from { background-color: #ffc107; } 4 | to { background-color: white; } 5 | } 6 | 7 | .fresh { 8 | animation-name: fresh; 9 | animation-duration: 30s; 10 | } 11 | 12 | .loader-container { 13 | margin: 20% auto 0 auto; 14 | text-align: center; 15 | } 16 | 17 | .loader { 18 | border: 16px solid #f3f3f3; /* Light grey */ 19 | border-top: 16px solid #3498db; /* Blue */ 20 | border-radius: 50%; 21 | width: 120px; 22 | height: 120px; 23 | animation: spin 2s linear infinite; 24 | display: inline-block; 25 | } 26 | 27 | @font-face { 28 | font-family: 'PT Sans Narrow'; 29 | font-style: normal; 30 | font-weight: 400; 31 | src: local('PT Sans Narrow'), local('PTSans-Narrow'), url('../fonts/pt-sans.woff2') format('woff2'); 32 | } 33 | 34 | @font-face { 35 | font-family: 'Ubuntu Mono'; 36 | font-style: normal; 37 | font-weight: 400; 38 | src: local('Ubuntu Mono'), local('UbuntuMono-Regular'), url('../fonts/ubuntu-mono.woff2') format('woff2'); 39 | } 40 | 41 | @font-face { 42 | font-family: 'Gentium Basic'; 43 | font-style: normal; 44 | font-weight: 400; 45 | src: local('Gentium Basic'), local('GentiumBasic'), url('../fonts/gentium-basic.woff2') format('woff2'); 46 | } 47 | 48 | @keyframes spin { 49 | 0% { transform: rotate(0deg); } 50 | 100% { transform: rotate(360deg); } 51 | } 52 | 53 | body, input { 54 | } 55 | 56 | body, input { 57 | } 58 | 59 | pre { 60 | font-family: 'Ubuntu Mono'; 61 | background: #333; 62 | overflow: auto; 63 | border: 1px solid #292929; 64 | border-radius: 4px; 65 | color: #ddd; 66 | padding: 6px 10px; 67 | } 68 | 69 | pre code a { 70 | color: #eef; 71 | } 72 | 73 | h1, h2 { 74 | } 75 | 76 | 77 | #view { 78 | margin: 5em auto 5em auto; 79 | 80 | } 81 | 82 | footer div { 83 | text-align: center; 84 | } 85 | 86 | svg { 87 | position: relative; 88 | } 89 | 90 | .badge { 91 | cursor: pointer; 92 | } 93 | 94 | .disabled { 95 | pointer-events: none; 96 | opacity: 0.4; 97 | cursor: not-allowed; 98 | } 99 | 100 | #signed_certificates .filterable .fa-circle { color: #888888; } 101 | #signed_certificates .filterable[data-state='online'] .fa-circle { color: #5cb85c; } 102 | #signed_certificates .filterable[data-state='offline'] .fa-circle { color: #0275d8; } 103 | #signed_certificates .filterable[data-state='dead'] .fa-circle { color: #d9534f; } 104 | 105 | -------------------------------------------------------------------------------- /certidude/static/fonts/gentium-basic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurivosandi/certidude/6e50c85c8587d3e800d01ce1d1588965c29b2c46/certidude/static/fonts/gentium-basic.woff2 -------------------------------------------------------------------------------- /certidude/static/fonts/pt-sans.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurivosandi/certidude/6e50c85c8587d3e800d01ce1d1588965c29b2c46/certidude/static/fonts/pt-sans.woff2 -------------------------------------------------------------------------------- /certidude/static/fonts/ubuntu-mono.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurivosandi/certidude/6e50c85c8587d3e800d01ce1d1588965c29b2c46/certidude/static/fonts/ubuntu-mono.woff2 -------------------------------------------------------------------------------- /certidude/static/img/ubuntu-01-edit-connections.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurivosandi/certidude/6e50c85c8587d3e800d01ce1d1588965c29b2c46/certidude/static/img/ubuntu-01-edit-connections.png -------------------------------------------------------------------------------- /certidude/static/img/ubuntu-02-network-connections.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurivosandi/certidude/6e50c85c8587d3e800d01ce1d1588965c29b2c46/certidude/static/img/ubuntu-02-network-connections.png -------------------------------------------------------------------------------- /certidude/static/img/ubuntu-03-import-saved-config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurivosandi/certidude/6e50c85c8587d3e800d01ce1d1588965c29b2c46/certidude/static/img/ubuntu-03-import-saved-config.png -------------------------------------------------------------------------------- /certidude/static/img/ubuntu-04-select-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurivosandi/certidude/6e50c85c8587d3e800d01ce1d1588965c29b2c46/certidude/static/img/ubuntu-04-select-file.png -------------------------------------------------------------------------------- /certidude/static/img/ubuntu-05-profile-imported.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurivosandi/certidude/6e50c85c8587d3e800d01ce1d1588965c29b2c46/certidude/static/img/ubuntu-05-profile-imported.png -------------------------------------------------------------------------------- /certidude/static/img/ubuntu-06-ipv4-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurivosandi/certidude/6e50c85c8587d3e800d01ce1d1588965c29b2c46/certidude/static/img/ubuntu-06-ipv4-settings.png -------------------------------------------------------------------------------- /certidude/static/img/ubuntu-07-disable-default-route.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurivosandi/certidude/6e50c85c8587d3e800d01ce1d1588965c29b2c46/certidude/static/img/ubuntu-07-disable-default-route.png -------------------------------------------------------------------------------- /certidude/static/img/ubuntu-08-activate-connection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurivosandi/certidude/6e50c85c8587d3e800d01ce1d1588965c29b2c46/certidude/static/img/ubuntu-08-activate-connection.png -------------------------------------------------------------------------------- /certidude/static/img/windows-01-download-openvpn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurivosandi/certidude/6e50c85c8587d3e800d01ce1d1588965c29b2c46/certidude/static/img/windows-01-download-openvpn.png -------------------------------------------------------------------------------- /certidude/static/img/windows-02-install-openvpn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurivosandi/certidude/6e50c85c8587d3e800d01ce1d1588965c29b2c46/certidude/static/img/windows-02-install-openvpn.png -------------------------------------------------------------------------------- /certidude/static/img/windows-03-move-config-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurivosandi/certidude/6e50c85c8587d3e800d01ce1d1588965c29b2c46/certidude/static/img/windows-03-move-config-file.png -------------------------------------------------------------------------------- /certidude/static/img/windows-04-connect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurivosandi/certidude/6e50c85c8587d3e800d01ce1d1588965c29b2c46/certidude/static/img/windows-04-connect.png -------------------------------------------------------------------------------- /certidude/static/img/windows-05-connected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurivosandi/certidude/6e50c85c8587d3e800d01ce1d1588965c29b2c46/certidude/static/img/windows-05-connected.png -------------------------------------------------------------------------------- /certidude/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Certidude server 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 34 |
35 |
36 |
37 |

Loading certificate authority...

38 |
39 |
40 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /certidude/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: / 3 | -------------------------------------------------------------------------------- /certidude/templates/bootstrap.conf: -------------------------------------------------------------------------------- 1 | # This will be merged to /etc/certidude/services.conf 2 | 3 | [Some old connection] 4 | managed = true 5 | enabled = false 6 | 7 | [Connection in another authority] 8 | refers = http://ca2.example.lan/api/bootstrap/ 9 | 10 | [Office LLC] 11 | managed = true 12 | enabled = true 13 | 14 | # Authority FQDN 15 | authority = {{ authority }} 16 | 17 | # Service to be configured on the client 18 | service = init/openvpn 19 | ;service = init/strongswan 20 | ;service = network-manager/openvpn 21 | ;service = network-manager/strongswan 22 | 23 | # Server addresses for the client 24 | remote ={% for server in servers %} {{ server }}{% endfor %} 25 | 26 | # To customize port number advertised for certidude bootstrap 27 | ;port = 1194 28 | 29 | # Protocol to advertise for certidude bootstrap 30 | ;proto = udp 31 | 32 | -------------------------------------------------------------------------------- /certidude/templates/client/certidude.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Renew certificates and update revocation lists 3 | 4 | [Service] 5 | Type=simple 6 | ExecStart={{ sys.argv[0] }} enroll 7 | -------------------------------------------------------------------------------- /certidude/templates/client/certidude.timer: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Run certidude enroll daily 3 | 4 | [Timer] 5 | OnCalendar=daily 6 | Persistent=true 7 | Unit=certidude-enroll.service 8 | 9 | [Install] 10 | WantedBy=timers.target 11 | 12 | -------------------------------------------------------------------------------- /certidude/templates/client/openvpn-reconnect.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Restart OpenVPN after suspend 3 | 4 | [Service] 5 | ExecStart=/usr/bin/pkill --signal SIGHUP --exact openvpn 6 | 7 | [Install] 8 | WantedBy=sleep.target 9 | -------------------------------------------------------------------------------- /certidude/templates/mail/certificate-renewed.md: -------------------------------------------------------------------------------- 1 | Renewed {{ common_name }} ({{ cert_serial_hex }}) 2 | 3 | This is simply to notify that certificate for {{ common_name }} 4 | was renewed and the serial number of the new certificate is {{ cert_serial_hex }}. 5 | 6 | The new certificate is valid from {{ builder.begin_date }} until 7 | {{ builder.end_date }}. 8 | 9 | Services making use of those certificates should continue working as expected. 10 | -------------------------------------------------------------------------------- /certidude/templates/mail/certificate-revoked.md: -------------------------------------------------------------------------------- 1 | Revoked {{ common_name }} ({{ serial_hex }}) 2 | 3 | This is simply to notify that certificate {{ common_name }} 4 | was revoked. 5 | 6 | Services making use of this certificates might become unavailable. 7 | -------------------------------------------------------------------------------- /certidude/templates/mail/certificate-signed.md: -------------------------------------------------------------------------------- 1 | Signed {{ common_name }} ({{ cert_serial_hex }}) 2 | 3 | This is simply to notify that certificate {{ common_name }} 4 | with serial number {{ cert_serial_hex }} 5 | was signed{% if signer %} by {{ signer }}{% endif %}. 6 | 7 | The certificate is valid from {{ builder.begin_date }} until 8 | {{ builder.end_date }}. 9 | 10 | {% if overwritten %} 11 | By doing so existing certificate with the same common name 12 | and serial number {{ prev_serial_hex }} was rejected 13 | and services making use of that certificate might become unavailable. 14 | {% endif %} 15 | -------------------------------------------------------------------------------- /certidude/templates/mail/expiration-notification.md: -------------------------------------------------------------------------------- 1 | {% if expired %}{{ expired | length }} have expired{% endif %}{% if expired and about_to_expire %}, {% endif %}{% if about_to_expire %}{{ about_to_expire | length }} about to expire{% endif %} 2 | 3 | {% if about_to_expire %} 4 | Following certificates are about to expire within following 48 hours: 5 | 6 | {% for common_name, path, cert in expired %} 7 | * {{ common_name }}, {{ "%x" % cert.serial_number }} 8 | {% endfor %} 9 | {% endif %} 10 | 11 | {% if expired %} 12 | Following certificates have expired: 13 | 14 | {% for common_name, path, cert in expired %} 15 | * {{ common_name }}, {{ "%x" % cert.serial_number }} 16 | {% endfor %} 17 | {% endif %} 18 | 19 | -------------------------------------------------------------------------------- /certidude/templates/mail/request-stored.md: -------------------------------------------------------------------------------- 1 | Stored request {{ common_name }} 2 | 3 | This is simply to notify that certificate signing request for {{ common_name }} 4 | was stored. You may log in with a certificate authority administration account to sign it. 5 | 6 | -------------------------------------------------------------------------------- /certidude/templates/mail/test.md: -------------------------------------------------------------------------------- 1 | Test mail 2 | 3 | Testing! 4 | -------------------------------------------------------------------------------- /certidude/templates/mail/token.md: -------------------------------------------------------------------------------- 1 | Token for {{ subject }} 2 | 3 | {% if issuer == subject %} 4 | Token has been issued for {{ subject }} for retrieving profile from link below. 5 | {% else %} 6 | {{ issuer }} has provided {{ subject }} a token for retrieving 7 | profile from the link below. 8 | {% endif %} 9 | 10 | Click here to claim the token. 11 | Token is usable until {{ token_expires }}{% if token_timezone %} ({{ token_timezone }} time){% endif %}. 12 | 13 | -------------------------------------------------------------------------------- /certidude/templates/openvpn-client.conf: -------------------------------------------------------------------------------- 1 | # Copy this file to /etc/certidude/template.ovpn and customize as you see fit 2 | 3 | # Note: don't append comments to lines, Ubuntu 16.04 NetworkManager importer is very picky 4 | # See more potential problems here: 5 | # https://askubuntu.com/questions/761684/error-the-plugin-does-not-support-import-capability-when-attempting-to-import 6 | 7 | # Run as OpenVPN client, pull routes, DNS server, DNS suffix from gateway 8 | client 9 | 10 | # OpenVPN gateway(s) 11 | nobind 12 | ;proto udp 13 | ;port 1194 14 | {% if servers %} 15 | remote-random 16 | {% for server in servers %} 17 | remote {{ server }} 18 | {% endfor %} 19 | {% else %} 20 | remote 1.2.3.4 21 | {% endif %} 22 | 23 | # Virtual network interface settings 24 | dev tun 25 | persist-tun 26 | 27 | # Customize crypto settings 28 | ;tls-version-min 1.2 29 | ;tls-cipher TLS-DHE-RSA-WITH-AES-256-GCM-SHA384 30 | ;cipher AES-256-CBC 31 | ;auth SHA384 32 | 33 | # Check that server presented certificate has TLS Server flag present 34 | remote-cert-tls server 35 | 36 | # X.509 business 37 | persist-key 38 | 39 | {{ca}} 40 | 41 | 42 | {{key}} 43 | 44 | 45 | {{cert}} 46 | 47 | 48 | # Revocation list 49 | # Tunnelblick doens't handle inlined CRL 50 | # hard to update as well 51 | ; 52 | ; 53 | 54 | # Pre-shared key for extra layer of security 55 | ; 56 | ; 57 | 58 | -------------------------------------------------------------------------------- /certidude/templates/script/default.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | {% if named_tags or other_tags %} 4 | # Tags: 5 | {% for key, value in named_tags.items() %} 6 | # {{ key }} -> {{ value }} 7 | {% endfor %} 8 | {% for tag in other_tags %} 9 | # {{ tag }} 10 | {% endfor %} 11 | {% else %} 12 | # No tags 13 | {% endif %} 14 | 15 | ARGS="kernel=$(uname -sr)&\ 16 | cpu=$(cat /proc/cpuinfo | grep '^model name' | head -n1 | cut -d ":" -f2 | xargs)&\ 17 | $(for j in /sys/class/net/[we]*[a-z][0-9]; do echo -en if.$(basename $j).ether=$(cat $j/address)\&; done)" 18 | 19 | if [ -e /etc/openwrt_release ]; then 20 | . /etc/openwrt_release 21 | ARGS="$ARGS&dist=$DISTRIB_ID $DISTRIB_RELEASE" 22 | else 23 | ARGS="$ARGS&dist=$(lsb_release -si) $(lsb_release -sr)" 24 | fi 25 | 26 | if [ -e /sys/class/dmi ]; then 27 | ARGS="$ARGS&dmi.product_name=$(cat /sys/class/dmi/id/product_name)&dmi.product_serial=$(cat /sys/class/dmi/id/product_serial)" 28 | ARGS="$ARGS&&mem=$(dmidecode -t 17 | grep Size | cut -d ":" -f 2 | cut -d " " -f 2 | paste -sd+ | bc) MB" 29 | else 30 | ARGS="$ARGS&dmi.product_name=$(cat /proc/cpuinfo | grep '^machine' | head -n 1 | cut -d ":" -f 2 | xargs)" 31 | ARGS="$ARGS&mem=$(expr $(cat /proc/meminfo | grep MemTotal | cut -d ":" -f 2 | xargs | cut -d " " -f 1) / 1024 + 1 ) MB" 32 | fi 33 | 34 | # Submit some stats to CA 35 | curl --cert-status https://{{ authority_name }}:8443/api/signed/{{ common_name }}/attr \ 36 | --cacert /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \ 37 | --key /etc/certidude/authority/{{ authority_name }}/host_key.pem \ 38 | --cert /etc/certidude/authority/{{ authority_name }}/host_cert.pem \ 39 | -X POST \-d "$ARGS" 40 | -------------------------------------------------------------------------------- /certidude/templates/script/openwrt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This script can be executed on a preconfigured OpenWrt box 4 | # https://lauri.vosandi.com/2017/01/reconfiguring-openwrt-as-dummy-ap.html 5 | 6 | # Password protected wireless area 7 | for band in 2ghz 5ghz; do 8 | uci set wireless.lan$band=wifi-iface 9 | uci set wireless.lan$band.network=lan 10 | uci set wireless.lan$band.mode=ap 11 | uci set wireless.lan$band.device=radio$band 12 | uci set wireless.lan$band.encryption=psk2 13 | {% if named_tags and named_tags.wireless and named_tags.wireless.protected and named_tags.wireless.protected.ssid %} 14 | uci set wireless.lan$band.ssid={{ named_tags.wireless.protected.ssid }} 15 | {% else %} 16 | uci set wireless.lan$band.ssid=$(uci get system.@system[0].hostname)-protected 17 | {% endif %} 18 | {% if named_tags and named_tags.wireless and named_tags.wireless.protected and named_tags.wireless.protected.psk %} 19 | uci set wireless.lan$band.key={{ named_tags.wireless.protected.psk }} 20 | {% else %} 21 | uci set wireless.lan$band.key=salakala 22 | {% endif %} 23 | done 24 | 25 | # Public wireless area 26 | for band in 2ghz 5ghz; do 27 | uci set wireless.guest$band=wifi-iface 28 | uci set wireless.guest$band.network=guest 29 | uci set wireless.guest$band.mode=ap 30 | uci set wireless.guest$band.device=radio$band 31 | uci set wireless.guest$band.encryption=none 32 | {% if named_tags and named_tags.wireless and named_tags.wireless.public and named_tags.wireless.public.ssid %} 33 | uci set wireless.guest$band.ssid={{ named_tags.wireless.public.ssid }} 34 | {% else %} 35 | uci set wireless.guest$band.ssid=$(uci get system.@system[0].hostname)-public 36 | {% endif %} 37 | done 38 | 39 | -------------------------------------------------------------------------------- /certidude/templates/script/workstation.sh: -------------------------------------------------------------------------------- 1 | # Submit some stats to CA 2 | curl http://{{ authority_name }}/api/signed/{{ common_name }}/attr -X POST -d "\ 3 | dmi.product_name=$(cat /sys/class/dmi/id/product_name)&\ 4 | dmi.product_serial=$(cat /sys/class/dmi/id/product_serial)&\ 5 | kernel=$(uname -sr)&\ 6 | dist=$(lsb_release -si) $(lsb_release -sr)&\ 7 | cpu=$(cat /proc/cpuinfo | grep '^model name' | head -n1 | cut -d ":" -f2 | xargs)&\ 8 | mem=$(dmidecode -t 17 | grep Size | cut -d ":" -f 2 | cut -d " " -f 2 | paste -sd+ | bc) MB&\ 9 | $(for j in /sys/class/net/[we]*; do echo -en if.$(basename $j).ether=$(cat $j/address)\&; done)" 10 | 11 | -------------------------------------------------------------------------------- /certidude/templates/server/backend.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Certidude server 3 | After=network.target 4 | 5 | [Service] 6 | Type=forking 7 | EnvironmentFile=/etc/environment 8 | Environment=LANG=C.UTF-8 9 | Environment=PYTHON_EGG_CACHE=/tmp/.cache 10 | PIDFile=/run/certidude/server.pid 11 | KillSignal=SIGINT 12 | ExecStart={{ certidude_path }} serve --fork 13 | TimeoutSec=15 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | 18 | -------------------------------------------------------------------------------- /certidude/templates/server/builder.conf: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | # LEDE image builder profiles enabled by default 3 | enabled = yes 4 | 5 | # Path to filesystem overlay used 6 | overlay = {{ builder_path }}/overlay 7 | 8 | # Hostname or regex to match the IPSec gateway included in the image 9 | router = ^(router|vpn|gw|gateway)\d*\. 10 | 11 | # Site specific script to be copied to /etc/uci-defaults/99-site-script 12 | # use it to include SSH keys, set passwords, etc 13 | script = /etc/certidude/script/site.sh 14 | 15 | # Which subnets are routed to the tunnel 16 | subnets = 192.168.0.0/16 172.16.0.0/12 10.0.0.0/8 17 | 18 | # Strongswan IKEv2 proposals 19 | ike=aes256-sha384-{{ dhgroup }}! 20 | esp=aes128gcm16-aes128gmac-{{ dhgroup }}! 21 | 22 | 23 | [tpl-wdr3600-factory] 24 | enabled = no 25 | 26 | # Title shown in the UI 27 | title = TP-Link WDR3600 (Access Point), TFTP-friendly 28 | 29 | # Script to build the image, copy file to /etc/certidude/script/ and make modifications as necessary 30 | command = /srv/certidude/doc/builder/ap.sh 31 | 32 | # Device/model/profile selection 33 | model = tl-wdr3600-v1 34 | 35 | # File that will be picked from the bin/ folder 36 | filename = tl-wdr3600-v1-squashfs-factory.bin 37 | 38 | # And renamed to make it TFTP-friendly 39 | rename = wdr4300v1_tp_recovery.bin 40 | 41 | 42 | 43 | [tpl-wdr4300-factory] 44 | enabled = no 45 | title = TP-Link WDR4300 (Access Point), TFTP-friendly 46 | command = /srv/certidude/doc/builder/ap.sh 47 | model = tl-wdr4300-v1 48 | filename = tl-wdr4300-v1-squashfs-factory.bin 49 | rename = wdr4300v1_tp_recovery.bin 50 | 51 | [tpl-archer-c7-factory] 52 | enabled = no 53 | title = TP-Link Archer C7 (Access Point), TFTP-friendly 54 | command = {{ builder_path }}/ap.sh 55 | model = archer-c7-v2 56 | filename = archer-c7-v2-squashfs-factory-eu.bin 57 | rename = ArcherC7v2_tp_recovery.bin 58 | 59 | [cf-e380ac-factory] 60 | enabled = no 61 | title = Comfast E380AC (Access Point), TFTP-friendly 62 | command = {{ builder_path }}/ap.sh 63 | model = cf-e380ac-v2 64 | filename = cf-e380ac-v2-squashfs-sysupgrade.bin 65 | rename = firmware_auto.bin 66 | 67 | 68 | 69 | [tpl-wdr3600-sysupgrade] 70 | ;enabled = yes 71 | title = TP-Link WDR3600 (Access Point) 72 | command = /srv/certidude/doc/builder/ap.sh 73 | model = tl-wdr3600-v1 74 | filename = tl-wdr3600-v1-squashfs-sysupgrade.bin 75 | rename = ap-tl-wdr3600-v1-squashfs-sysupgrade.bin 76 | 77 | 78 | [tpl-wdr4300-sysupgrade] 79 | ;enabled = yes 80 | title = TP-Link WDR4300 (Access Point) 81 | command = /srv/certidude/doc/builder/ap.sh 82 | model = tl-wdr4300-v1 83 | filename = tl-wdr4300-v1-squashfs-sysupgrade.bin 84 | rename = ap-tl-wdr4300-v1-squashfs-sysupgrade.bin 85 | 86 | [tpl-archer-c7-sysupgrade] 87 | ;enabled = yes 88 | title = TP-Link Archer C7 (Access Point) 89 | command = {{ builder_path }}/ap.sh 90 | model = archer-c7-v2 91 | filename = archer-c7-v2-squashfs-factory-eu.bin 92 | rename = ap-archer-c7-v2-squashfs-factory-eu.bin 93 | 94 | [cf-e380ac-sysupgrade] 95 | ;enabled = yes 96 | title = Comfast E380AC (Access Point) 97 | command = {{ builder_path }}/ap.sh 98 | model = cf-e380ac-v2 99 | filename = cf-e380ac-v2-squashfs-factory.bin 100 | rename = ap-cf-e380ac-v2-squashfs-factory.bin 101 | 102 | [ar150-mfp-sysupgrade] 103 | ;enabled = yes 104 | title = GL.iNet GL-AR150 (MFP) 105 | command = {{ builder_path }}/mfp.sh 106 | model = gl-ar150 107 | filename = ar71xx-generic-gl-ar150-squashfs-sysupgrade.bin 108 | rename = mfp-gl-ar150-squashfs-sysupgrade.bin 109 | 110 | [ar150-cam-sysupgrade] 111 | ;enabled = yes 112 | title = GL.iNet GL-AR150 (IP Camera) 113 | command = {{ builder_path }}/ipcam.sh 114 | model = gl-ar150 115 | filename = ar71xx-generic-gl-ar150-squashfs-sysupgrade.bin 116 | rename = cam-gl-ar150-squashfs-sysupgrade.bin 117 | -------------------------------------------------------------------------------- /certidude/templates/server/housekeeping-daily.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Run daily housekeeping jobs, eg certificate expiration notifications 3 | 4 | [Service] 5 | Type=oneshot 6 | ExecStart={{ certidude_path }} housekeeping daily 7 | 8 | -------------------------------------------------------------------------------- /certidude/templates/server/housekeeping-daily.timer: -------------------------------------------------------------------------------- 1 | [Timer] 2 | Persistent=true 3 | OnCalendar=daily 4 | 5 | [Install] 6 | WantedBy=multi-user.target 7 | -------------------------------------------------------------------------------- /certidude/templates/server/ldap-kinit.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Initialize Kerberos credential cache for LDAP connections of Certidude 3 | Before=certidude-backend.service 4 | 5 | [Service] 6 | Type=oneshot 7 | ExecStart={{ certidude_path }} housekeeping kinit 8 | -------------------------------------------------------------------------------- /certidude/templates/server/ldap-kinit.timer: -------------------------------------------------------------------------------- 1 | [Timer] 2 | Persistent=true 3 | OnBootSec=3sec 4 | OnUnitActiveSec=8h 5 | 6 | [Install] 7 | WantedBy=multi-user.target 8 | -------------------------------------------------------------------------------- /certidude/templates/server/nginx.conf: -------------------------------------------------------------------------------- 1 | # Basic DoS prevention measures 2 | limit_conn addr 10; 3 | client_body_timeout 5s; 4 | client_header_timeout 5s; 5 | limit_conn_zone $binary_remote_addr zone=addr:10m; 6 | 7 | # Backend configuration 8 | proxy_set_header Host $host; 9 | proxy_set_header X-Real-IP $remote_addr; 10 | proxy_set_header X-SSL-CERT $ssl_client_cert; 11 | proxy_connect_timeout 600; 12 | proxy_send_timeout 600; 13 | proxy_read_timeout 600; 14 | send_timeout 600; 15 | 16 | # Don't buffer any messages 17 | nchan_message_buffer_length 0; 18 | 19 | # To use CA-s own certificate for frontend and mutually authenticated connections 20 | ssl_certificate {{ directory }}/signed/{{ common_name }}.pem; 21 | ssl_certificate_key {{ directory }}/self_key.pem; 22 | 23 | server { 24 | # Uncomment following to automatically redirect to HTTPS 25 | #rewrite ^/$ https://$server_name$request_uri? permanent; 26 | 27 | # Section for serving insecure HTTP, note that this is suitable for 28 | # OCSP, SCEP, CRL-s etc which is already covered by PKI protection mechanisms. 29 | # This also solves the chicken-and-egg problem of deploying the certificates 30 | 31 | server_name {{ common_name }}; 32 | listen 80 default_server; 33 | 34 | # Proxy pass CRL server 35 | location /api/revoked/ { 36 | proxy_pass http://127.0.1.1:8082/api/revoked/; 37 | } 38 | 39 | # Proxy pass OCSP responder 40 | location /api/ocsp/ { 41 | proxy_pass http://127.0.1.1:8081/api/ocsp/; 42 | } 43 | 44 | # Proxy pass to backend 45 | location /api/ { 46 | proxy_pass http://127.0.1.1:8080/api/; 47 | } 48 | 49 | # Path to compiled assets 50 | location /assets/ { 51 | alias {{ assets_dir }}/; 52 | } 53 | 54 | # Rewrite /cgi-bin/pkiclient.exe to /api/scep for SCEP protocol 55 | location /cgi-bin/pkiclient.exe { 56 | rewrite /cgi-bin/pkiclient.exe /api/scep/ last; 57 | } 58 | 59 | {% if not push_server %} 60 | # Long poll for CSR submission 61 | location ~ "^/lp/sub/(.*)" { 62 | nchan_channel_id $1; 63 | nchan_subscriber longpoll; 64 | } 65 | {% endif %} 66 | 67 | # Comment everything below in this server definition if you're using HTTPS 68 | 69 | {% if not push_server %} 70 | # Event source for web interface 71 | location ~ "^/ev/sub/(.*)" { 72 | nchan_channel_id $1; 73 | nchan_subscriber eventsource; 74 | } 75 | {% endif %} 76 | 77 | # Path to static files 78 | root {{static_path}}; 79 | error_page 502 /502.json; 80 | 81 | access_log /var/log/nginx/certidude-plaintext-access.log; 82 | error_log /var/log/nginx/certidude-plaintext-error.log; 83 | } 84 | 85 | server { 86 | # Section for accessing web interface over HTTPS 87 | listen 443 ssl http2 default_server; 88 | server_name {{ common_name }}; 89 | 90 | # To use Let's Encrypt certificates 91 | {% if not letsencrypt %}#{% endif %}ssl_certificate {{ letsencrypt_fullchain }}; 92 | {% if not letsencrypt %}#{% endif %}ssl_certificate_key {{ letsencrypt_privkey }}; 93 | 94 | # Also run the following to set up Let's Encrypt certificates: 95 | # 96 | # apt install letsencrypt 97 | # certbot certonly -d {{common_name}} --webroot /var/www/html/ 98 | 99 | # HSTS header below should make sure web interface will be accessed over HTTPS only 100 | # once it has been configured 101 | add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;"; 102 | 103 | # Proxy pass image builder 104 | location /api/log/ { 105 | proxy_pass http://127.0.1.1:8084/api/log/; 106 | } 107 | 108 | # Proxy pass image builder 109 | location /api/builder/ { 110 | proxy_pass http://127.0.1.1:8083/api/builder/; 111 | } 112 | 113 | # Proxy pass to backend 114 | location /api/ { 115 | proxy_pass http://127.0.1.1:8080/api/; 116 | } 117 | 118 | # Path to compiled assets 119 | location /assets/ { 120 | alias {{ assets_dir }}/; 121 | } 122 | 123 | # This is for Let's Encrypt enroll/renewal 124 | location /.well-known/ { 125 | alias /var/www/html/.well-known/; 126 | } 127 | 128 | {% if not push_server %} 129 | # Event stream for pushing events to web browsers 130 | location ~ "^/ev/sub/(.*)" { 131 | nchan_channel_id $1; 132 | nchan_subscriber eventsource; 133 | } 134 | 135 | # Long poll for CSR submission 136 | location ~ "^/lp/sub/(.*)" { 137 | nchan_channel_id $1; 138 | nchan_subscriber longpoll; 139 | } 140 | {% endif %} 141 | 142 | # Path to static files 143 | root {{static_path}}; 144 | error_page 502 /502.json; 145 | 146 | access_log /var/log/nginx/certidude-frontend-access.log; 147 | error_log /var/log/nginx/certidude-frontend-error.log; 148 | } 149 | 150 | 151 | server { 152 | # Section for certificate authenticated HTTPS clients, 153 | # for submitting information to CA eg. leases, 154 | # requesting/renewing certificates and 155 | # for delivering scripts to clients 156 | 157 | server_name {{ common_name }}; 158 | listen 8443 ssl http2; 159 | 160 | # Enforce OCSP stapling for the server certificate 161 | # Note that even nginx 1.14.0 doesn't immideately populate the OCSP cache 162 | # You need to run separate cronjob to populate the OCSP response cache 163 | ssl_stapling on; 164 | ssl_stapling_verify on; 165 | 166 | # Allow client authentication with certificate, 167 | # backend must still check if certificate was used for TLS handshake 168 | ssl_verify_client optional; 169 | ssl_client_certificate {{ authority_path }}; 170 | 171 | # Proxy pass to backend 172 | location /api/ { 173 | proxy_pass http://127.0.1.1:8080/api/; 174 | } 175 | 176 | # Long poll 177 | location ~ "^/lp/sub/(.*)" { 178 | nchan_channel_id $1; 179 | nchan_subscriber longpoll; 180 | } 181 | 182 | # Path to static files 183 | root {{static_path}}; 184 | error_page 502 /502.json; 185 | 186 | access_log /var/log/nginx/certidude-mutual-auth-access.log; 187 | error_log /var/log/nginx/certidude-mutual-auth-error.log; 188 | } 189 | 190 | {% if not push_server %} 191 | server { 192 | # Allow publishing only from localhost to prevent abuse 193 | server_name localhost; 194 | listen 127.0.0.1:80; 195 | 196 | location ~ "^/lp/pub/(.*)" { 197 | nchan_publisher; 198 | nchan_channel_id $1; 199 | } 200 | 201 | location ~ "^/ev/pub/(.*)" { 202 | nchan_publisher; 203 | nchan_channel_id $1; 204 | } 205 | 206 | access_log /var/log/nginx/certidude-push-access.log; 207 | error_log /var/log/nginx/certidude-push-error.log; 208 | } 209 | {% endif %} 210 | 211 | -------------------------------------------------------------------------------- /certidude/templates/server/profile.conf: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | enabled = no 3 | ou = 4 | lifetime = 120 5 | ca = false 6 | common name = RE_COMMON_NAME 7 | key usage = digital_signature key_encipherment 8 | extended key usage = 9 | 10 | # Strongswan can automatically fetch CRL if 11 | # CRL distribution point extension is included in the certificate 12 | ;revoked url = 13 | revoked url = {{ revoked_url }} 14 | 15 | # StrongSwan can automatically query OCSP responder if 16 | # AIA extension includes OCSP responder URL 17 | ;responder url = 18 | ;responder url = no check 19 | responder url = {{ responder_url }} 20 | 21 | [ca] 22 | enabled = yes 23 | title = Certificate Authority 24 | common name = ^ca 25 | ca = true 26 | key usage = key_cert_sign crl_sign 27 | extended key usage = 28 | lifetime = 1095 29 | 30 | [rw] 31 | enabled = yes 32 | title = Roadwarrior 33 | ou = Roadwarrior 34 | common name = RE_HOSTNAME 35 | extended key usage = client_auth 36 | 37 | [srv] 38 | enabled = yes 39 | title = Server 40 | ou = Server 41 | common name = RE_FQDN 42 | lifetime = 120 43 | extended key usage = server_auth client_auth 44 | 45 | [gw] 46 | enabled = yes 47 | title = Gateway 48 | ou = Gateway 49 | common name = RE_FQDN 50 | renewable = true 51 | lifetime = 120 52 | extended key usage = server_auth 1.3.6.1.5.5.8.2.2 client_auth 53 | 54 | [ap] 55 | enabled = no 56 | title = Access Point 57 | ou = Access Point 58 | common name = RE_HOSTNAME 59 | lifetime = 120 60 | extended key usage = client_auth 61 | 62 | [mfp] 63 | enabled = no 64 | title = Printers 65 | ou = MFP 66 | common name = ^mfp\- 67 | lifetime = 120 68 | extended key usage = client_auth 69 | 70 | [cam] 71 | enabled = no 72 | title = Camera 73 | ou = IP Camera 74 | common name = ^cam\- 75 | lifetime = 120 76 | extended key usage = client_auth 77 | 78 | [ocsp] 79 | enabled = no 80 | title = OCSP Responder 81 | common name = ^ocsp 82 | lifetime = 7 83 | responder url = nocheck 84 | -------------------------------------------------------------------------------- /certidude/templates/server/responder.service: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurivosandi/certidude/6e50c85c8587d3e800d01ce1d1588965c29b2c46/certidude/templates/server/responder.service -------------------------------------------------------------------------------- /certidude/templates/server/server.conf: -------------------------------------------------------------------------------- 1 | [authentication] 2 | # The authentiction backend specifies how the user is authenticated, 3 | # in case of 'pam' simplepam.authenticate is used to authenticate against 4 | # sshd PAM service. In case of 'kerberos' SPNEGO is used to authenticate 5 | # user against eg. Active Directory or Samba4. 6 | 7 | ;backends = ldap 8 | ;backends = kerberos 9 | {% if realm %} 10 | backends = kerberos ldap 11 | ;backends = pam 12 | {% else %} 13 | ;backends = kerberos ldap 14 | backends = pam 15 | {% endif %} 16 | 17 | kerberos keytab = FILE:{{ kerberos_keytab }} 18 | {% if realm %} 19 | # Kerberos realm derived from /etc/samba/smb.conf 20 | kerberos realm = {{ realm }} 21 | {% else %} 22 | # Kerberos realm 23 | kerberos realm = EXAMPLE.LAN 24 | {% endif %} 25 | 26 | {% if domain %} 27 | # LDAP URI derived from /etc/samba/smb.conf 28 | ldap uri = ldaps://dc1.{{ domain }} 29 | {% else %} 30 | # Placeholder LDAP URI 31 | ldap uri = ldaps://dc1.example.lan 32 | {% endif %} 33 | 34 | [accounts] 35 | # The accounts backend specifies how the user's given name, surname and e-mail 36 | # address are looked up. In case of 'posix' basically 'getent passwd' is performed, 37 | # in case of 'ldap' a search is performed on LDAP server specified by ldap uri 38 | # with Kerberos credential cache initialized at path specified by environment variable KRB5CCNAME 39 | # If certidude setup authority was performed correctly the credential cache should be 40 | # updated automatically by /etc/cron.hourly/certidude 41 | 42 | {% if not realm %} 43 | backend = posix 44 | {% else %} 45 | ;backend = posix 46 | {% endif %} 47 | mail suffix = example.lan 48 | 49 | {% if realm %} 50 | backend = ldap 51 | {% else %} 52 | ;backend = ldap 53 | {% endif %} 54 | ldap gssapi credential cache = /run/certidude/krb5cc 55 | 56 | {% if domain %} 57 | # LDAP URI derived from /etc/samba/smb.conf 58 | ldap uri = ldap://dc1.{{ domain }} 59 | {% else %} 60 | # LDAP URI 61 | ldap uri = ldaps://dc1.example.lan 62 | {% endif %} 63 | 64 | {% if base %} 65 | # LDAP base derived from /etc/samba/smb.conf 66 | ldap base = {{ base }} 67 | {% else %} 68 | ldap base = dc=example,dc=lan 69 | {% endif %} 70 | 71 | ldap mail attribute = mail 72 | ;ldap mail attribute = otherMailbox 73 | 74 | [authorization] 75 | # The authorization backend specifies how the users are authorized. 76 | # In case of 'posix' simply group membership is asserted, 77 | # in case of 'ldap' search filter with username as placeholder is applied. 78 | 79 | {% if realm %} 80 | ;backend = posix 81 | {% else %} 82 | backend = posix 83 | {% endif %} 84 | posix user group = users 85 | posix admin group = sudo 86 | 87 | {% if realm %} 88 | backend = ldap 89 | {% else %} 90 | ;backend = ldap 91 | {% endif %} 92 | ldap computer filter = (&(objectclass=user)(objectclass=computer)(samaccountname=%s)) 93 | ldap user filter = (&(objectclass=user)(objectcategory=person)(samaccountname=%s)) 94 | {% if base %} 95 | # LDAP user filter for administrative accounts, derived from /etc/samba/smb.conf 96 | ldap admin filter = (&(memberOf=cn=Domain Admins,cn=Users,{{ base }})(samaccountname=%s)) 97 | {% else %} 98 | # LDAP user filter for administrative accounts 99 | ldap admin filter = (&(memberOf=cn=Domain Admins,cn=Users,dc=example,dc=lan)(samaccountname=%s)) 100 | {% endif %} 101 | ;ldap admin filter = (&(samaccountname=lauri)(samaccountname=%s)) 102 | 103 | ;backend = whitelist 104 | user whitelist = 105 | admin whitelist = 106 | 107 | # Users are allowed to log in from user subnets 108 | user subnets = 0.0.0.0/0 109 | 110 | # Certificate signing requests are allowed to be submitted from these subnets 111 | request subnets = 0.0.0.0/0 112 | 113 | # Certificates are automatically signed for these subnets 114 | autosign subnets = 127.0.0.0/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 115 | 116 | # Simple Certificate Enrollment Protocol enabled subnets 117 | scep subnets = 118 | ;scep subnets = 0.0.0.0/0 119 | 120 | # Online Certificate Status Protocol enabled subnets, anywhere by default 121 | ;ocsp subnets = 122 | ocsp subnets = 0.0.0.0/0 123 | 124 | # Certificate Revocation lists can be accessed from anywhere by default 125 | ;crl subnets = 126 | crl subnets = 0.0.0.0/0 127 | 128 | # If certificate renewal is attempted from whitelisted subnets, clients can 129 | # request a certificate for the same public key with extended lifetime. 130 | # To disable set to none 131 | renewal subnets = 132 | ;renewal subnets = 0.0.0.0/0 133 | 134 | # From which subnets autosign and SCEP requests are allowed to overwrite 135 | # already existing certificate with same CN 136 | overwrite subnets = 137 | ;overwrite subnets = 0.0.0.0/0 138 | 139 | 140 | # Which subnets are offered Kerberos authentication, eg. 141 | # subnet for Windows workstations or slice of VPN subnet where 142 | # workstations are assigned to 143 | kerberos subnets = 0.0.0.0 144 | ;kerberos subnets = 145 | 146 | 147 | # Source subnets of Kerberos authenticated machines which are automatically 148 | # allowed to enroll with CSR whose common name is set to machine's account name. 149 | # Note that overwriting is not allowed by default, see 'overwrite subnets' 150 | # option above 151 | machine enrollment subnets = 152 | ;machine enrollment subnets = 0.0.0.0/0 153 | 154 | 155 | # Authenticated users belonging to administrative LDAP or POSIX group 156 | # are allowed to sign and revoke certificates from these subnets 157 | admin subnets = 0.0.0.0/0 158 | ;admin subnets = 172.20.7.0/24 172.20.8.5 159 | 160 | 161 | [logging] 162 | # Disable logging 163 | ;backend = 164 | 165 | # Use SQLite backend 166 | backend = sql 167 | database = sqlite://{{ directory }}/meta/db.sqlite 168 | 169 | [signature] 170 | # Server certificate is granted to certificate with 171 | # common name that includes period which translates to FQDN of the machine. 172 | # TLS Server Auth and IKE Intermediate flags are attached to such certificate. 173 | # Due to problematic CRL support in client applications 174 | # we keep server certificate lifetime short and 175 | # have it renewed automatically. 176 | server certificate lifetime = 3 177 | 178 | # Client certificates are granted to everything else 179 | # TLS Client Auth flag is attached to such certificate. 180 | # In this case it's set to 4 months. 181 | client certificate lifetime = 120 182 | 183 | revocation list lifetime = 24 184 | 185 | # URL where CA certificate can be fetched from 186 | authority certificate url = {{ certificate_url }} 187 | 188 | 189 | [push] 190 | # This should occasionally be regenerated 191 | event source token = {{ push_token }} 192 | 193 | # For local nchan 194 | event source publish = http://localhost/ev/pub/%s 195 | long poll publish = http://localhost/lp/pub/%s 196 | event source subscribe = /ev/sub/%s 197 | long poll subscribe = /lp/sub/%s 198 | 199 | # For remote nchan, make sure you use https:// if SSL is configured on push server 200 | ;event source publish = http://push.example.com/ev/pub/%s 201 | ;long poll publish = http://push.example.com/lp/pub/%s 202 | ;event source subscribe = //push.example.com/ev/sub/%s 203 | ;long poll subscribe = //push.example.com/lp/sub/%s 204 | 205 | [authority] 206 | # Present form for CSR submission for logged in users 207 | ;request submission allowed = true 208 | request submission allowed = false 209 | 210 | # User certificate enrollment specifies whether logged in users are allowed to 211 | # request bundles. In case of 'single allowed' the common name of the 212 | # certificate is set to username, this should work well with REMOTE_USER 213 | # enabled web apps running behind Apache/nginx. 214 | # In case of 'multiple allowed' the common name is set to username@device-identifier. 215 | ;user enrollment = forbidden 216 | ;user enrollment = single allowed 217 | user enrollment = multiple allowed 218 | 219 | # Certificate authority keypair 220 | private key path = {{ ca_key }} 221 | certificate path = {{ authority_path }} 222 | 223 | # Private key used by nginx frontend 224 | self key path = {{ self_key }} 225 | 226 | # Directories for requests, signed, revoked and expired certificates 227 | requests dir = {{ directory }}/requests/ 228 | signed dir = {{ directory }}/signed/ 229 | revoked dir = {{ directory }}/revoked/ 230 | expired dir = {{ directory }}/expired/ 231 | 232 | [mailer] 233 | # Certidude submits mails to local MTA. 234 | # In case of Postfix configure it as "Sattelite system", 235 | # and make sure Certidude machine doesn't try to accept mails. 236 | # uncomment mail sender address to enable e-mails. 237 | # Make sure used e-mail address is reachable for end users. 238 | name = Certidude at {{ common_name }} 239 | {% if domain %} 240 | address = certificates@{{ domain }} 241 | {% else %} 242 | address = certificates@example.com 243 | {% endif %} 244 | 245 | [tagging] 246 | owner/string = Owner 247 | location/string = Location 248 | phone/string = Phone 249 | other/ = Other 250 | 251 | [bootstrap] 252 | # Following can be used to set up clients easily: certidude bootstrap ca.example.lan 253 | # Services template is rendered on certidude server with relevant variables and 254 | # placed to /etc/certidude/services.conf on the client 255 | services template = {{ template_path }}/bootstrap.conf 256 | 257 | [token] 258 | # Token mechanism allows authority administrator to send invites for users. 259 | # Backend for tokens, set none to disable 260 | ;backend = 261 | backend = sql 262 | 263 | # Database path for SQL backend 264 | database = sqlite://{{ directory }}/meta/db.sqlite 265 | 266 | # URL format, router and protocols are substituted from the [service] section below 267 | url = https://{{ common_name }}/#action=enroll&title=certidude.rocks&token=%(token)s&subject=%(subject_username)s&router=%(router)s&protocols=%(protocols)s 268 | 269 | # Token lifetime in minutes, 48 hours by default. 270 | # Note that code tolerates 5 minute clock skew. 271 | lifetime = 2880 272 | 273 | # Whether token allows overwriting certificate with same CN 274 | ;overwrite permitted = yes 275 | overwrite permitted = no 276 | 277 | 278 | [script] 279 | # Path to the folder with scripts that can be served to the clients, set none to disable scripting 280 | path = {{ script_dir }} 281 | ;path = /etc/certidude/script 282 | ;path = 283 | 284 | [service] 285 | protocols = ikev2 https openvpn 286 | routers = ^(router|vpn|gw|gateway)\d*\. 287 | -------------------------------------------------------------------------------- /certidude/templates/server/site.sh: -------------------------------------------------------------------------------- 1 | # Configure port tagging 2 | uci set network.lan.ifname='eth0.3' # Protected network VLAN3 tagged 3 | uci set network.guest.ifname='eth0.4' # Public network VLAN4 tagged 4 | 5 | # Configure wireless networks 6 | for band in 2ghz 5ghz; do 7 | uci delete wireless.radio$band.disabled 8 | uci set wireless.radio$band.country=EE 9 | 10 | uci set wireless.guest$band=wifi-iface 11 | uci set wireless.guest$band.network=guest 12 | uci set wireless.guest$band.mode=ap 13 | uci set wireless.guest$band.device=radio$band 14 | uci set wireless.guest$band.encryption=none 15 | uci set wireless.guest$band.ssid="k-space.ee guest" 16 | 17 | uci set wireless.lan$band=wifi-iface 18 | uci set wireless.lan$band.network=lan 19 | uci set wireless.lan$band.mode=ap 20 | uci set wireless.lan$band.device=radio$band 21 | uci set wireless.lan$band.encryption=psk2+ccmp 22 | uci set wireless.lan$band.ssid="k-space protected" 23 | uci set wireless.lan$band.key="salakala" 24 | 25 | done 26 | 27 | # Add Lauri's Yubikey 28 | cat > /etc/dropbear/authorized_keys << \EOF 29 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCb4iqSrJrA13ygAZTZb6ElPsMXrlXXrztxt3bcKuEbAiWOm9lR17puRLMZbM2tvAW+iwsDHfQAs0E6HDprP68nt+SGkQvItUtYeJBWDI405DbRodmDMySahmb6o6S3sqI4vryydOg1G+Z0DITksZzp91Ow+C++emk6aqWfXh7xATexCvKphfwXrBL+MDIwx6drIiN0FD08yd/zxGAlcQpR8o6uecmXdk32wL5W3+qqwbJrLjZmOweij5KSXuEARuQhM20KXzYzzQIAKqhIoALRSEX31L0bwxOqfVaotzk4TWKJSeetEhBOd7PtH0ZrmOHF+B20Ym+V3UkRY5P4calF 30 | EOF 31 | 32 | # Set root password to 'salakala' 33 | sed -i 's|^root::|root:$1$S0wGaZqK$fzEzb0WTC5.WHm2Fz9UI9.:|' /etc/shadow 34 | 35 | -------------------------------------------------------------------------------- /certidude/templates/snippets/ansible-site.yml: -------------------------------------------------------------------------------- 1 | - hosts: {% for router in session.service.routers %} 2 | {{ router }}{% endfor %} 3 | 4 | roles: 5 | - role: certidude 6 | authority_name: {{ session.authority.hostname }} 7 | 8 | - role: ipsec_mesh 9 | mesh_name: mymesh 10 | authority_name: {{ session.authority.hostname }} 11 | ike: aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}! 12 | esp: aes128gcm16-aes128gmac-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}! 13 | auto: start 14 | nodes:{% for router in session.service.routers %} 15 | {{ router }}: 172.27.{{ loop.index }}.0/24{% endfor %} 16 | -------------------------------------------------------------------------------- /certidude/templates/snippets/certidude-client.sh: -------------------------------------------------------------------------------- 1 | pip3 install git+https://github.com/laurivosandi/certidude/ 2 | mkdir -p /etc/certidude/{client.conf.d,services.conf.d} 3 | 4 | cat << \EOF > /etc/certidude/client.conf.d/{{ session.authority.hostname }}.conf 5 | [{{ session.authority.hostname }}] 6 | trigger = interface up 7 | common name = $HOSTNAME 8 | system wide = true 9 | EOF 10 | 11 | cat << EOF > /etc/certidude/services.conf.d/{{ session.authority.hostname }}.conf{% for router in session.service.routers %}{% if "ikev2" in session.service.protocols %} 12 | [IPSec to {{ router }}] 13 | authority = {{ session.authority.hostname }} 14 | service = network-manager/strongswan 15 | remote = {{ router }} 16 | {% endif %}{% if "openvpn" in session.service.protocols %} 17 | [OpenVPN to {{ router }}] 18 | authority = {{ session.authority.hostname }} 19 | service = network-manager/openvpn 20 | remote = {{ router }} 21 | {% endif %}{% endfor %}EOF 22 | 23 | certidude enroll 24 | 25 | -------------------------------------------------------------------------------- /certidude/templates/snippets/gateway-updown.sh: -------------------------------------------------------------------------------- 1 | # Create VPN gateway up/down script for reporting client IP addresses to CA 2 | cat <<\EOF > /etc/certidude/authority/{{ session.authority.hostname }}/updown 3 | #!/bin/sh 4 | 5 | CURL="curl --cert-status -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/" 6 | 7 | case $PLUTO_VERB in 8 | up-client) $CURL --data-urlencode "outer_address=$PLUTO_PEER" --data-urlencode "inner_address=$PLUTO_PEER_SOURCEIP" --data-urlencode "client=$PLUTO_PEER_ID" ;; 9 | *) ;; 10 | esac 11 | 12 | case $script_type in 13 | client-connect) $CURL --data-urlencode client=$X509_0_CN --data-urlencode serial=$tls_serial_0 --data-urlencode outer_address=$untrusted_ip --data-urlencode inner_address=$ifconfig_pool_remote_ip ;; 14 | *) ;; 15 | esac 16 | EOF 17 | 18 | chmod +x /etc/certidude/authority/{{ session.authority.hostname }}/updown 19 | 20 | -------------------------------------------------------------------------------- /certidude/templates/snippets/ios.mobileconfig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PayloadDisplayName 6 | {{ gateway }} 7 | 8 | PayloadIdentifier 9 | {{ gateway }} 10 | PayloadUUID 11 | {{ service_uuid }} 12 | PayloadType 13 | Configuration 14 | PayloadVersion 15 | 1 16 | PayloadContent 17 | 18 | 19 | PayloadIdentifier 20 | {{ gateway }}.conf1 21 | PayloadUUID 22 | {{ conf_uuid }} 23 | PayloadType 24 | com.apple.vpn.managed 25 | PayloadVersion 26 | 1 27 | UserDefinedName 28 | {{ gateway }} 29 | VPNType 30 | IKEv2 31 | IKEv2 32 | 33 | RemoteAddress 34 | {{ gateway }} 35 | RemoteIdentifier 36 | {{ gateway }} 37 | LocalIdentifier 38 | {{ common_name }} 39 | ServerCertificateIssuerCommonName 40 | {{ session.authority.certificate.common_name }} 41 | ServerCertificateCommonName 42 | {{ gateway }} 43 | AuthenticationMethod 44 | Certificate 45 | IKESecurityAssociationParameters 46 | 47 | EncryptionAlgorithm 48 | AES-256 49 | IntegrityAlgorithm 50 | SHA2-384 51 | DiffieHellmanGroup 52 | 14 53 | 54 | ChildSecurityAssociationParameters 55 | 56 | EncryptionAlgorithm 57 | AES-128-GCM 58 | IntegrityAlgorithm 59 | SHA2-256 60 | DiffieHellmanGroup 61 | 14 62 | 63 | EnablePFS 64 | 1 65 | PayloadCertificateUUID 66 | {{ p12_uuid }} 67 | 68 | 69 | 70 | PayloadIdentifier 71 | {{ common_name }} 72 | PayloadUUID 73 | {{ p12_uuid }} 74 | PayloadType 75 | com.apple.security.pkcs12 76 | PayloadVersion 77 | 1 78 | PayloadContent 79 | {{ p12 }} 80 | 81 | 82 | PayloadIdentifier 83 | {{ session.authority.certificate.common_name }} 84 | PayloadUUID 85 | {{ ca_uuid }} 86 | PayloadType 87 | com.apple.security.root 88 | PayloadVersion 89 | 1 90 | PayloadContent 91 | {{ ca }} 92 | 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /certidude/templates/snippets/networkmanager-openvpn.conf: -------------------------------------------------------------------------------- 1 | [connection] 2 | certidude managed = true 3 | id = {{ session.service.title }} 4 | uuid = {{ uuid }} 5 | type = vpn 6 | 7 | [vpn] 8 | service-type = org.freedesktop.NetworkManager.openvpn 9 | connection-type = tls 10 | cert-pass-flags 0 11 | tap-dev = no 12 | remote-cert-tls = server 13 | remote = {{ routers[0] }} 14 | key = {% if key_path %}{{ key_path }}{% else %}/etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem{% endif %} 15 | cert = {% if certificate_path %}{{ certificate_path }}{% else %}/etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem{% endif %} 16 | ca = {% if authority_path %}{{ authority_path }}{% else %}/etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem{% endif %} 17 | tls-cipher = TLS-{% if authority_public_key.algorithm == "ec" %}ECDHE-ECDSA{% else %}DHE-RSA{% endif %}-WITH-AES-256-GCM-SHA384 18 | cipher = AES-128-GCM 19 | auth = SHA384 20 | {% if port %};port = {{ port }}{% else %};port = 1194{% endif %} 21 | {% if not proto or not proto.startswith('tcp') %};{% endif %}proto-tcp = yes 22 | 23 | [ipv4] 24 | # Route only pushed subnets to tunnel 25 | never-default = true 26 | method = auto 27 | 28 | [ipv6] 29 | method = auto 30 | -------------------------------------------------------------------------------- /certidude/templates/snippets/networkmanager-strongswan.conf: -------------------------------------------------------------------------------- 1 | [connection] 2 | certidude managed = true 3 | id = {{ session.service.title }} 4 | uuid = {{ uuid }} 5 | type = {{ vpn }} 6 | 7 | [vpn] 8 | service-type = org.freedesktop.NetworkManager.strongswan 9 | encap = no 10 | virtual = yes 11 | method = key 12 | ipcomp = no 13 | address = {{ session.service.routers[0] }} 14 | userkey = {% if key_path %}{{ key_path }}{% else %}/etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem{% endif %} 15 | usercert = {% if certificate_path %}{{ certificate_path }}{% else %}/etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem{% endif %} 16 | certificate = {% if authority_path %}{{ authority_path }}{% else %}/etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem{% endif %} 17 | ike = aes256-sha384-prfsha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %} 18 | esp = aes128gcm16-aes128gmac-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %} 19 | proposal = yes 20 | 21 | [ipv4] 22 | method = auto 23 | ;route1 = 0.0.0.0/0 24 | -------------------------------------------------------------------------------- /certidude/templates/snippets/nginx-https-site.conf: -------------------------------------------------------------------------------- 1 | 2 | server { 3 | listen 80; 4 | server_name {{ common_name }}; 5 | rewrite ^ https://{{ common_name }}\$request_uri?; 6 | } 7 | 8 | server { 9 | root /var/www/html; 10 | add_header X-Frame-Options "DENY"; 11 | add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload"; 12 | listen 443 ssl; 13 | server_name $NAME; 14 | client_max_body_size 10G; 15 | ssl_certificate {{certificate_path}}; 16 | ssl_certificate_key {{key_path}}; 17 | ssl_client_certificate {{authority_path}}; 18 | 19 | # Uncomment following to enable mutual authentication with certificates 20 | #ssl_crl {{revocations_path}}; 21 | #ssl_verify_client on; 22 | 23 | location ~ \.php\$ { 24 | fastcgi_split_path_info ^(.+\.php)(/.+)$; 25 | fastcgi_pass unix:/run/php5-fpm.sock; 26 | fastcgi_index index.php; 27 | fastcgi_param REMOTE_USER \$ssl_client_s_dn_cn; 28 | include fastcgi_params; 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /certidude/templates/snippets/nginx-ocsp-cache.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Cache OCSP responses for nginx OCSP stapling 3 | Requires=nginx.service 4 | 5 | [Service] 6 | Type=oneshot 7 | ExecStart=-/usr/bin/curl --cert-status https://{{ session.authority.hostname }}:8443/ --cacert /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem 8 | -------------------------------------------------------------------------------- /certidude/templates/snippets/nginx-ocsp-cache.timer: -------------------------------------------------------------------------------- 1 | [Timer] 2 | OnCalendar=*:0/15 3 | Persistent=true 4 | -------------------------------------------------------------------------------- /certidude/templates/snippets/nginx-tls.conf: -------------------------------------------------------------------------------- 1 | # Configure secure defaults for nginx 2 | ssl_dhparam {{ dhparam_path }}; 3 | 4 | # Note that depending on the certificate type (RSA, ECDSA) this will be even further constrained: 5 | 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; 6 | 7 | ssl_ecdh_curve secp384r1; 8 | ssl_session_timeout 10m; 9 | ssl_session_cache shared:SSL:10m; 10 | ssl_session_tickets off; 11 | add_header X-Frame-Options DENY; 12 | add_header X-Content-Type-Options nosniff; 13 | add_header X-XSS-Protection "1; mode=block"; 14 | add_header X-Robots-Tag none; 15 | 16 | # Following are already enabled by /etc/nginx/nginx.conf 17 | #ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 18 | #ssl_prefer_server_ciphers on; 19 | 20 | # Add SSLUserName SSL_CLIENT_S_DN_CN style parameter support 21 | map $ssl_client_s_dn $ssl_client_s_dn_cn { 22 | default ""; 23 | ~/CN=([^/]+) $1; 24 | } 25 | 26 | -------------------------------------------------------------------------------- /certidude/templates/snippets/ocsp-cache@.service: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurivosandi/certidude/6e50c85c8587d3e800d01ce1d1588965c29b2c46/certidude/templates/snippets/ocsp-cache@.service -------------------------------------------------------------------------------- /certidude/templates/snippets/openvpn-client.conf: -------------------------------------------------------------------------------- 1 | client 2 | nobind{% for router in session.service.routers %} 3 | remote {{ router }}{% endfor %} 4 | proto tcp-client 5 | port 443 6 | tls-version-min 1.2 7 | tls-cipher TLS-{% if session.authority.certificate.algorithm == "ec" %}ECDHE-ECDSA{% else %}DHE-RSA{% endif %}-WITH-AES-256-GCM-SHA384 8 | cipher AES-128-GCM 9 | auth SHA384 10 | mute-replay-warnings 11 | reneg-sec 0 12 | remote-cert-tls server 13 | dev tun 14 | persist-tun 15 | persist-key 16 | {% if ca %} 17 | 18 | {{ ca }} 19 | 20 | {% else %}ca /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem{% endif %} 21 | {% if key %} 22 | 23 | {{ key }} 24 | 25 | {% else %}key /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem{% endif %} 26 | {% if cert %} 27 | 28 | {{ cert }} 29 | 30 | {% else %}cert /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem{% endif %} 31 | 32 | # To enable dynamic DNS server update on Ubuntu, uncomment these 33 | #script-security 2 34 | #up /etc/openvpn/update-resolv-conf 35 | #down /etc/openvpn/update-resolv-conf 36 | -------------------------------------------------------------------------------- /certidude/templates/snippets/openvpn-client.sh: -------------------------------------------------------------------------------- 1 | # Install packages on Ubuntu & Fedora 2 | which apt && apt install openvpn 3 | which dnf && dnf install openvpn 4 | 5 | # Create OpenVPN configuration file 6 | cat > /etc/openvpn/{{ session.authority.hostname }}.conf << EOF 7 | {% include "snippets/openvpn-client.conf" %} 8 | EOF 9 | 10 | # Restart OpenVPN service 11 | systemctl restart openvpn 12 | {# 13 | 14 | Some notes: 15 | 16 | - Ubuntu 16.04 ships OpenVPN 2.3 which doesn't support AES-128-GCM 17 | - NetworkManager's OpenVPN profile importer doesn't understand multiple remotes 18 | - Tunnelblick and OpenVPN Connect apps don't have a method to update CRL 19 | 20 | #} 21 | -------------------------------------------------------------------------------- /certidude/templates/snippets/openwrt-openvpn.sh: -------------------------------------------------------------------------------- 1 | opkg update 2 | opkg install curl openssl-util openvpn-openssl 3 | 4 | {% if session.authority.certificate.algorithm != "ec" %} 5 | # Generate Diffie-Hellman parameters file for OpenVPN 6 | test -e /etc/certidude/dh.pem \ 7 | || openssl dhparam 2048 -out /etc/certidude/dh.pem 8 | {% endif %} 9 | # Create interface definition for tunnel 10 | uci set network.vpn=interface 11 | uci set network.vpn.name='vpn' 12 | uci set network.vpn.ifname=tun_s2c_udp tun_s2c_tcp 13 | uci set network.vpn.proto='none' 14 | 15 | # Create zone definition for VPN interface 16 | uci set firewall.vpn=zone 17 | uci set firewall.vpn.name='vpn' 18 | uci set firewall.vpn.input='ACCEPT' 19 | uci set firewall.vpn.forward='ACCEPT' 20 | uci set firewall.vpn.output='ACCEPT' 21 | uci set firewall.vpn.network='vpn' 22 | 23 | # Allow UDP 1194 on WAN interface 24 | uci set firewall.openvpn=rule 25 | uci set firewall.openvpn.name='Allow OpenVPN' 26 | uci set firewall.openvpn.src='wan' 27 | uci set firewall.openvpn.dest_port=1194 28 | uci set firewall.openvpn.proto='udp' 29 | uci set firewall.openvpn.target='ACCEPT' 30 | 31 | # Allow TCP 443 on WAN interface 32 | uci set firewall.openvpn=rule 33 | uci set firewall.openvpn.name='Allow OpenVPN over TCP' 34 | uci set firewall.openvpn.src='wan' 35 | uci set firewall.openvpn.dest_port=443 36 | uci set firewall.openvpn.proto='tcp' 37 | uci set firewall.openvpn.target='ACCEPT' 38 | 39 | # Forward traffic from VPN to LAN 40 | uci set firewall.c2s=forwarding 41 | uci set firewall.c2s.src='vpn' 42 | uci set firewall.c2s.dest='lan' 43 | 44 | # Permit DNS queries from VPN 45 | uci set dhcp.@dnsmasq[0].localservice='0' 46 | 47 | touch /etc/config/openvpn 48 | 49 | # Configure OpenVPN over TCP 50 | uci set openvpn.s2c_tcp=openvpn 51 | uci set openvpn.s2c_tcp.local=$(uci get network.wan.ipaddr) 52 | uci set openvpn.s2c_tcp.server='10.179.43.0 255.255.255.128' 53 | uci set openvpn.s2c_tcp.proto='tcp-server' 54 | uci set openvpn.s2c_tcp.port='443' 55 | uci set openvpn.s2c_tcp.dev=tun_s2c_tcp 56 | 57 | # Configure OpenVPN over UDP 58 | uci set openvpn.s2c_udp=openvpn 59 | uci set openvpn.s2c_udp.local=$(uci get network.wan.ipaddr) 60 | uci set openvpn.s2c_udp.server='10.179.43.128 255.255.255.128' 61 | uci set openvpn.s2c_tcp.dev=tun_s2c_udp 62 | 63 | for section in s2c_tcp s2c_udp; do 64 | 65 | # Common paths 66 | uci set openvpn.$section.script_security=2 67 | uci set openvpn.$section.client_connect='/etc/certidude/updown' 68 | uci set openvpn.$section.key='/etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem' 69 | uci set openvpn.$section.cert='/etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem' 70 | uci set openvpn.$section.ca='/etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem' 71 | {% if session.authority.certificate.algorithm != "ec" %}uci set openvpn.$section.dh='/etc/certidude/dh.pem'{% endif %} 72 | uci set openvpn.$section.enabled=1 73 | 74 | # DNS and routes 75 | uci add_list openvpn.$section.push="route-metric 1000" 76 | uci add_list openvpn.$section.push="route $(uci get network.lan.ipaddr) $(uci get network.lan.netmask)" 77 | uci add_list openvpn.$section.push="dhcp-option DNS $(uci get network.lan.ipaddr)" 78 | uci add_list openvpn.$section.push="dhcp-option DOMAIN $(uci get dhcp.@dnsmasq[0].domain)" 79 | 80 | # Security hardening 81 | uci set openvpn.$section.tls_version_min='1.2' 82 | uci set openvpn.$section.tls_cipher='TLS-{% if session.authority.certificate.algorithm == "ec" %}ECDHE-ECDSA{% else %}DHE-RSA{% endif %}-WITH-AES-128-GCM-SHA384' 83 | uci set openvpn.$section.cipher='AES-128-GCM' 84 | uci set openvpn.$section.auth='SHA384' 85 | 86 | done 87 | 88 | /etc/init.d/openvpn restart 89 | /etc/init.d/firewall restart 90 | -------------------------------------------------------------------------------- /certidude/templates/snippets/renew.sh: -------------------------------------------------------------------------------- 1 | curl --cert-status -f -L -H "Content-type: application/pkcs10" \ 2 | --cacert /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem \ 3 | --key /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem \ 4 | --cert /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem \ 5 | --data-binary @/etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem \ 6 | -o /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem \ 7 | 'https://{{ session.authority.hostname }}:8443/api/request/?wait=yes' 8 | -------------------------------------------------------------------------------- /certidude/templates/snippets/request-client.ps1: -------------------------------------------------------------------------------- 1 | # Generate keypair and submit CSR 2 | {% if common_name %}$NAME = "{{ common_name }}" 3 | {% else %}$NAME = $env:computername.toLower() 4 | {% endif %} 5 | @" 6 | [NewRequest] 7 | Subject = "CN=$NAME" 8 | Exportable = FALSE 9 | KeySpec = 1 10 | KeyUsage = 0xA0 11 | MachineKeySet = True 12 | ProviderType = 12 13 | RequestType = PKCS10 14 | {% if session.authority.certificate.algorithm == "ec" %}ProviderName = "Microsoft Software Key Storage Provider" 15 | KeyAlgorithm = ECDSA_P384 16 | {% else %}ProviderName = "Microsoft RSA SChannel Cryptographic Provider" 17 | KeyLength = 2048 18 | {% endif %}"@ | Out-File req.inf 19 | C:\Windows\system32\certreq.exe -new -f -q req.inf host_csr.pem 20 | Invoke-WebRequest `{% if token %} 21 | -Uri 'https://{{ session.authority.hostname }}:8443/api/token/?token={{ token }}' ` 22 | -Method PUT `{% else %} 23 | -Uri 'https://{{ session.authority.hostname }}:8443/api/request/?wait=yes&autosign=yes' ` 24 | -Method POST `{% endif %} 25 | -TimeoutSec 900 ` 26 | -InFile host_csr.pem ` 27 | -ContentType application/pkcs10 ` 28 | -MaximumRedirection 3 -OutFile host_cert.pem 29 | 30 | # Import certificate 31 | Import-Certificate -FilePath host_cert.pem -CertStoreLocation Cert:\LocalMachine\My 32 | {# 33 | 34 | On Windows 7 the Import-Certificate cmdlet is missing, 35 | but certutil.exe can be used instead: 36 | 37 | C:\Windows\system32\certutil.exe -addstore My host_cert.pem 38 | 39 | Everything seems to work except after importing the certificate 40 | it is not properly associated with the private key, 41 | that means "You have private key that corresponds to this certificate" is not 42 | shown under "Valid from ... to ..." in MMC. 43 | This results in error code 13806 during IKEv2 handshake and error message 44 | "IKE failed to find valid machine certificate" 45 | 46 | #} 47 | 48 | -------------------------------------------------------------------------------- /certidude/templates/snippets/request-client.sh: -------------------------------------------------------------------------------- 1 | # Use short hostname as common name 2 | test -e /sbin/uci && NAME=$(uci get system.@system[0].hostname) 3 | test -e /bin/hostname && NAME=$(hostname) 4 | test -n "$NAME" || NAME=$(cat /proc/sys/kernel/hostname) 5 | 6 | {% include "snippets/request-common.sh" %} 7 | # Submit CSR and save signed certificate 8 | curl --cert-status -f -L -H "Content-type: application/pkcs10" \ 9 | --data-binary @/etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem \ 10 | -o /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem \ 11 | 'http://{{ session.authority.hostname }}/api/request/?wait=yes&autosign=yes' 12 | -------------------------------------------------------------------------------- /certidude/templates/snippets/request-common.sh: -------------------------------------------------------------------------------- 1 | # Delete CA certificate if checksum doesn't match 2 | echo {{ session.authority.certificate.md5sum }} /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem | md5sum -c \ 3 | || rm -fv /etc/certidude/authority/{{ session.authority.hostname }}/*.pem 4 | {% include "snippets/store-authority.sh" %} 5 | {% include "snippets/update-trust.sh" %} 6 | # Generate private key 7 | test -e /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem \ 8 | || {% if session.authority.certificate.algorithm == "ec" %}openssl ecparam -name secp384r1 -genkey -noout \ 9 | -out /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem{% else %}openssl genrsa \ 10 | -out /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem 2048{% endif %} 11 | test -e /etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem \ 12 | || openssl req -new -sha384 -subj "/CN=$NAME" \ 13 | -key /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem \ 14 | -out /etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem 15 | echo "If CSR submission fails, you can copy paste it to Certidude:" 16 | cat /etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem 17 | -------------------------------------------------------------------------------- /certidude/templates/snippets/request-server.sh: -------------------------------------------------------------------------------- 1 | # Use fully qualified name 2 | test -e /sbin/uci && NAME=$(nslookup $(uci get network.wan.ipaddr) | grep "name =" | head -n1 | cut -d "=" -f 2 | xargs) 3 | test -e /bin/hostname && NAME=$(hostname -f) 4 | test -n "$NAME" || NAME=$(cat /proc/sys/kernel/hostname) 5 | 6 | {% include "snippets/request-common.sh" %} 7 | {% include "snippets/submit-request-wait.sh" %} 8 | -------------------------------------------------------------------------------- /certidude/templates/snippets/setup-ocsp-caching.sh: -------------------------------------------------------------------------------- 1 | # See more on http://unmitigatedrisk.com/?p=241 why we're doing this 2 | cat << EOF > /etc/systemd/system/nginx-ocsp-cache.service 3 | {% include "snippets/nginx-ocsp-cache.service" %}EOF 4 | 5 | cat << EOF > /etc/systemd/system/nginx-ocsp-cache.timer 6 | {% include "snippets/nginx-ocsp-cache.timer" %}EOF 7 | 8 | systemctl enable nginx-ocsp-cache.service 9 | systemctl enable nginx-ocsp-cache.timer 10 | systemctl start nginx-ocsp-cache.service 11 | systemctl start nginx-ocsp-cache.timer 12 | -------------------------------------------------------------------------------- /certidude/templates/snippets/store-authority.sh: -------------------------------------------------------------------------------- 1 | # Save CA certificate 2 | mkdir -p /etc/certidude/authority/{{ session.authority.hostname }}/ 3 | test -e /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem \ 4 | || cat << EOF > /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem 5 | {{ session.authority.certificate.blob }}EOF 6 | -------------------------------------------------------------------------------- /certidude/templates/snippets/strongswan-client.sh: -------------------------------------------------------------------------------- 1 | cat > /etc/ipsec.conf << EOF 2 | config setup 3 | strictcrlpolicy=yes 4 | 5 | ca {{ session.authority.hostname }} 6 | auto=add 7 | cacert=/etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem 8 | 9 | conn client-to-site 10 | auto=start 11 | right={{ session.service.routers[0] }} 12 | rightsubnet=0.0.0.0/0 13 | rightca="{{ session.authority.certificate.distinguished_name }}" 14 | left=%defaultroute 15 | leftcert=/etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem 16 | leftsourceip=%config 17 | leftca="{{ session.authority.certificate.distinguished_name }}" 18 | keyexchange=ikev2 19 | keyingtries=%forever 20 | dpdaction=restart 21 | closeaction=restart 22 | ike=aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}! 23 | esp=aes128gcm16-aes128gmac-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}! 24 | EOF 25 | 26 | echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} {{ session.authority.hostname }}.pem" > /etc/ipsec.secrets 27 | 28 | ipsec restart 29 | -------------------------------------------------------------------------------- /certidude/templates/snippets/strongswan-patching.sh: -------------------------------------------------------------------------------- 1 | # Install packages on Ubuntu & Fedora, patch Fedora paths 2 | which apt && apt install strongswan 3 | which dnf && dnf install strongswan 4 | test -e /etc/strongswan && test -e /etc/ipsec.conf || ln -s strongswan/ipsec.conf /etc/ipsec.conf 5 | test -e /etc/strongswan && test -e /etc/ipsec.d || ln -s strongswan/ipsec.d /etc/ipsec.d 6 | test -e /etc/strongswan && test -e /etc/ipsec.secrets || ln -s strongswan/ipsec.secrets /etc/ipsec.secrets 7 | 8 | # Set SELinux context 9 | chcon --type=home_cert_t /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem /etc/ipsec.d/cacerts/{{ session.authority.hostname }}.pem 10 | chcon --type=home_cert_t /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem /etc/ipsec.d/certs/{{ session.authority.hostname }}.pem 11 | chcon --type=home_cert_t /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem /etc/ipsec.d/private/{{ session.authority.hostname }}.pem 12 | 13 | # Patch AppArmor 14 | cat << EOF > /etc/apparmor.d/local/usr.lib.ipsec.charon 15 | /etc/certidude/authority/** r, 16 | EOF 17 | systemctl restart apparmor 18 | -------------------------------------------------------------------------------- /certidude/templates/snippets/strongswan-server.sh: -------------------------------------------------------------------------------- 1 | # Generate StrongSwan config 2 | cat > /etc/ipsec.conf << EOF 3 | config setup 4 | strictcrlpolicy=yes 5 | uniqueids=yes 6 | 7 | ca {{ session.authority.hostname }} 8 | auto=add 9 | cacert=/etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem 10 | 11 | conn default-{{ session.authority.hostname }} 12 | ike=aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}! 13 | esp=aes128gcm16-aes128gmac-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}! 14 | left=$(uci get network.wan.ipaddr) # Bind to this IP address 15 | leftid={{ session.service.routers | first }} 16 | leftupdown=/etc/certidude/authority/{{ session.authority.hostname }}/updown 17 | leftcert=/etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem 18 | leftsubnet=$(uci get network.lan.ipaddr | cut -d . -f 1-3).0/24 # Subnets pushed to roadwarriors 19 | leftca="{{ session.authority.certificate.distinguished_name }}" 20 | rightca="{{ session.authority.certificate.distinguished_name }}" 21 | rightsourceip=172.21.0.0/24 # Roadwarrior virtual IP pool 22 | dpddelay=0 23 | dpdaction=clear 24 | fragmentation=yes 25 | reauth=no 26 | rekey=no 27 | leftsendcert=always 28 | 29 | conn s2c-rw 30 | auto=add 31 | also=default-{{ session.authority.hostname }} 32 | rightdns=$(uci get network.lan.ipaddr) # IP of DNS server advertised to roadwarriors 33 | 34 | conn s2c-client1 35 | auto=ignore 36 | also=default-{{ session.authority.hostname }} 37 | rightid="CN=*, OU=IP Camera, O=*, DC=*, DC=*, DC=*" 38 | rightsourceip=172.21.0.1 39 | 40 | EOF 41 | 42 | echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem" > /etc/ipsec.secrets 43 | 44 | -------------------------------------------------------------------------------- /certidude/templates/snippets/submit-request-wait.sh: -------------------------------------------------------------------------------- 1 | # Submit CSR and save signed certificate 2 | curl --cert-status -f -L -H "Content-type: application/pkcs10" \ 3 | --cacert /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem \ 4 | --data-binary @/etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem \ 5 | -o /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem \ 6 | 'https://{{ session.authority.hostname }}:8443/api/request/?wait=yes' 7 | -------------------------------------------------------------------------------- /certidude/templates/snippets/update-trust.ps1: -------------------------------------------------------------------------------- 1 | # Install CA certificate 2 | @" 3 | {{ session.authority.certificate.blob }}"@ | Out-File ca_cert.pem 4 | Import-Certificate -FilePath ca_cert.pem -CertStoreLocation Cert:\LocalMachine\Root 5 | -------------------------------------------------------------------------------- /certidude/templates/snippets/update-trust.sh: -------------------------------------------------------------------------------- 1 | # Insert into Fedora trust store. Applies to curl, Firefox, Chrome, Chromium 2 | test -e /etc/pki/ca-trust/source/anchors \ 3 | && ln -s /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem /etc/pki/ca-trust/source/anchors/{{ session.authority.hostname }} \ 4 | && update-ca-trust 5 | 6 | # Insert into Ubuntu trust store, only applies to curl 7 | test -e /usr/local/share/ca-certificates/ \ 8 | && ln -f -s /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem /usr/local/share/ca-certificates/{{ session.authority.hostname }}.crt \ 9 | && update-ca-certificates 10 | 11 | # Patch Firefox trust store on Ubuntu 12 | if [ -d /usr/lib/firefox ]; then 13 | if [ ! -h /usr/lib/firefox/libnssckbi.so ]; then 14 | apt install -y p11-kit p11-kit-modules 15 | mv /usr/lib/firefox/libnssckbi.so /usr/lib/firefox/libnssckbi.so.bak 16 | ln -s /usr/lib/x86_64-linux-gnu/pkcs11/p11-kit-trust.so /usr/lib/firefox/libnssckbi.so 17 | fi 18 | fi 19 | -------------------------------------------------------------------------------- /certidude/templates/snippets/windows.ps1: -------------------------------------------------------------------------------- 1 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 2 | 3 | {% include "snippets/update-trust.ps1" %} 4 | 5 | {% include "snippets/request-client.ps1" %} 6 | 7 | {% for router in session.service.routers %} 8 | # Set up IPSec VPN tunnel to {{ router }} 9 | Remove-VpnConnection -AllUserConnection -Force "IPSec to {{ router }}" 10 | Add-VpnConnection ` 11 | -Name "IPSec to {{ router }}" ` 12 | -ServerAddress {{ router }} ` 13 | -AuthenticationMethod MachineCertificate ` 14 | -EncryptionLevel Maximum ` 15 | -SplitTunneling ` 16 | -TunnelType ikev2 ` 17 | -PassThru -AllUserConnection 18 | 19 | # Harden VPN configuration 20 | Set-VpnConnectionIPsecConfiguration ` 21 | -ConnectionName "IPSec to {{ router }}" ` 22 | -AuthenticationTransformConstants GCMAES128 ` 23 | -CipherTransformConstants GCMAES128 ` 24 | -EncryptionMethod AES256 ` 25 | -IntegrityCheckMethod SHA384 ` 26 | -DHGroup {% if session.authority.certificate.algorithm == "ec" %}ECP384{% else %}Group14{% endif %} ` 27 | -PfsGroup {% if session.authority.certificate.algorithm == "ec" %}ECP384{% else %}PFS2048{% endif %} ` 28 | -PassThru -AllUserConnection -Force 29 | {% endfor %} 30 | 31 | {# 32 | AuthenticationTransformConstants - ESP integrity algorithm, one of: None MD596 SHA196 SHA256128 GCMAES128 GCMAES192 GCMAES256 33 | CipherTransformConstants - ESP symmetric cipher, one of: DES DES3 AES128 AES192 AES256 GCMAES128 GCMAES192 GCMAES256 34 | EncryptionMethod - IKE symmetric cipher, one of: DES DES3 AES128 AES192 AES256 35 | IntegrityCheckMethod - IKE hash algorithm, one of: MD5 SHA196 SHA256 SHA384 36 | DHGroup = IKE key exchange, one of: None Group1 Group2 Group14 ECP256 ECP384 Group24 37 | PfsGroup = ESP key exchange, one of: None PFS1 PFS2 PFS2048 ECP256 ECP384 PFSMM PFS24 38 | #} 39 | -------------------------------------------------------------------------------- /certidude/templates/strongswan-site-to-client.conf: -------------------------------------------------------------------------------- 1 | # /etc/ipsec.conf - strongSwan IPsec configuration file 2 | 3 | # left/local = gateway 4 | # right/remote = client 5 | 6 | config setup 7 | cachecrls=yes 8 | strictcrlpolicy=yes 9 | 10 | conn %default 11 | ikelifetime=60m 12 | keylife=20m 13 | rekeymargin=3m 14 | keyingtries=1 15 | keyexchange=ikev2 16 | 17 | conn site-to-clients 18 | auto=ignore 19 | right=%any # Allow connecting from any IP address 20 | rightsourceip={{subnet}} # Serve virtual IP-s from this pool 21 | left={{common_name}} # Gateway IP address 22 | leftcert={{certificate_path}} # Gateway certificate 23 | {% if route %} 24 | {% if route | length == 1 %} 25 | leftsubnet={{route[0]}} # Advertise routes via this connection 26 | {% else %} 27 | leftsubnet={ {{ route | join(', ') }} } 28 | {% endif %} 29 | {% endif %} 30 | 31 | -------------------------------------------------------------------------------- /certidude/templates/views/attributes.html: -------------------------------------------------------------------------------- 1 | {% for key, value in certificate.attributes %} 2 | {{ value }} 3 | {% endfor %} 4 | -------------------------------------------------------------------------------- /certidude/templates/views/configuration.html: -------------------------------------------------------------------------------- 1 | 2 |

Create a rule

3 |

4 | 5 | 6 | 7 | 8 | 9 | Filter 10 | 11 | attaches attribute 12 | 15 | something 16 | 17 |

18 | 19 | {% for grouper, items in configuration | groupby('tag_id') %} 20 | 21 |

Filter {{ items[0].match_key }} is {{ items[0].match_value }}

22 | 28 | 29 | {% endfor %} 30 | 31 | 32 | -------------------------------------------------------------------------------- /certidude/templates/views/error.html: -------------------------------------------------------------------------------- 1 |

{{ message.title }}

2 |

{{ message.description }}

3 | -------------------------------------------------------------------------------- /certidude/templates/views/insecure.html: -------------------------------------------------------------------------------- 1 |

You're viewing this page over insecure channel. 2 | You can give it a try and connect over HTTPS, 3 | if that succeeds all subsequents accesses of this page will go over HTTPS. 4 |

5 |

6 | Click here to fetch the certificate of this authority. 7 | Alternatively install certificate on Fedora or Ubuntu with following copy-pastable snippet: 8 |

9 | 10 |
11 |
{% include "snippets/store-authority.sh" %}
12 | {% include "snippets/update-trust.sh" %}
13 |
14 | 15 | -------------------------------------------------------------------------------- /certidude/templates/views/lease.html: -------------------------------------------------------------------------------- 1 | Last seen 2 | 3 | at 4 | {{ certificate.lease.inner_address }}{% if certificate.lease.outer_address %} 5 | from 6 | {{ certificate.lease.outer_address }}{% endif %}. 7 | See some stats here. 8 | -------------------------------------------------------------------------------- /certidude/templates/views/logentry.html: -------------------------------------------------------------------------------- 1 |
  • 2 | 3 | 4 | {{ entry.message }} 5 | 6 | {{ entry.created }} 7 |
  • 8 | 9 | -------------------------------------------------------------------------------- /certidude/templates/views/request.html: -------------------------------------------------------------------------------- 1 |
    3 |
    4 | {% if certificate.server %} 5 | 6 | {% else %} 7 | 8 | {% endif %} 9 | {{ request.common_name }} 10 |
    11 |
    12 |

    13 | Submitted 14 | 15 | from 16 | {% if request.hostname %}{{request.hostname}} ({{request.address}}){% else %}{{request.address}}{% endif %} 17 |

    18 |
    19 | 20 | 24 | 28 | 31 | 39 |
    40 |
    41 |

    Use following to fetch the signing request:

    42 |
    43 |
    wget http://{{ session.authority.hostname }}/api/request/{{ request.common_name }}/
    44 | curl http://{{ session.authority.hostname }}/api/request/{{ request.common_name }}/ \
    45 |   | openssl req -text -noout
    46 |
    47 | 48 |
    49 | 50 | 51 | 52 | 55 | 56 | 57 | 58 | 59 |
    Common name{{ request.common_name }}
    Submitted{{ request.submitted | datetime }} 53 | {% if request.address %}from {{ request.address }} 54 | {% if request.hostname %} ({{ request.hostname }}){% endif %}{% endif %}
    MD5{{ request.md5sum }}
    SHA1{{ request.sha1sum }}
    SHA256{{ request.sha256sum }}
    60 |
    61 | 62 |
    63 |
    64 |
    65 | -------------------------------------------------------------------------------- /certidude/templates/views/revoked.html: -------------------------------------------------------------------------------- 1 |
    3 |
    4 |
    5 | {% if certificate.server %} 6 | 7 | {% else %} 8 | 9 | {% endif %} 10 | {{ certificate.common_name }} 11 |
    12 |
    13 |

    14 | Serial number {{ certificate.serial | serial }}. 15 |

    16 |

    17 | Revoked 18 | . 19 | Valid from {{ certificate.signed | datetime }} to {{ certificate.expired | datetime }}. 20 |

    21 | 22 |
    23 | 24 |
    25 | Download 26 |
    27 |
    28 |
    29 |

    To fetch certificate:

    30 | 31 |
    32 |
    wget http://{{ session.authority.hostname }}/api/revoked/{{ certificate.serial }}/
    33 | curl http://{{ session.authority.hostname }}/api/revoked/{{ certificate.serial }}/ \
    34 |   | openssl x509 -text -noout
    35 |
    36 | 37 |

    To perform online certificate status request

    38 |
    curl http://{{ session.authority.hostname }}/api/certificate/ > session.pem
    39 | openssl ocsp -issuer session.pem -CAfile session.pem \
    40 |   -url http://{{ session.authority.hostname }}/api/ocsp/ \
    41 |   -serial 0x{{ certificate.serial }}
    42 | 43 |

    44 | 45 | 46 | 47 | 48 | 49 | 51 | 52 | {% if certificate.lease %} 53 | 56 | {% endif %} 57 | 58 | 62 | 63 | 64 |
    Common name{{ certificate.common_name }}
    Organizational unit{{ certificate.organizational_unit }}
    Serial number{{ certificate.serial }}
    Signed{{ certificate.signed | datetime }} 50 | {% if certificate.signer %}, by {{ certificate.signer }}{% endif %}
    Expired{{ certificate.expired | datetime }}
    Lease{{ certificate.lease.inner_address }} at {{ certificate.lease.last_seen | datetime }} 54 | from {{ certificate.lease.outer_address }} 55 |
    SHA256{{ certificate.sha256sum }}
    65 |

    66 |
    67 |
    68 |
    69 |
    70 | -------------------------------------------------------------------------------- /certidude/templates/views/signed.html: -------------------------------------------------------------------------------- 1 |
    4 |
    5 | {% if certificate.organizational_unit %} 6 | 7 | {{ certificate.organizational_unit }} / 8 | {% endif %} 9 | {% if certificate.extensions.extended_key_usage and "server_auth" in certificate.extensions.extended_key_usage %} 10 | 11 | {% else %} 12 | 13 | {% endif %} 14 | {{ certificate.common_name }} 15 |
    16 |
    17 |

    18 | 19 | 20 | {% if certificate.lease %} 21 | {% include "views/lease.html" %} 22 | {% endif %} 23 | 24 | 25 | Signed 26 | {% if certificate.signer %} by {{ certificate.signer }}{% endif %}, 27 | expires 28 | . 29 |

    30 |

    31 | {% if session.authority.tagging %} 32 | 33 | {% include "views/tags.html" %} 34 | 35 | {% endif %} 36 | 37 | {% include "views/attributes.html" %} 38 | 39 |

    40 | 41 |
    42 | 43 | 46 | 49 | 57 |
    58 | 59 |
    60 | {% if session.authority.tagging %} 61 | 63 | 66 | 72 | {% endif %} 73 |
    74 | 75 |
    76 |

    To fetch certificate:

    77 | 78 |
    79 |
    wget http://{{ session.authority.hostname }}/api/signed/{{ certificate.common_name }}/
     80 | curl http://{{ session.authority.hostname }}/api/signed/{{ certificate.common_name }}/ \
     81 |   | openssl x509 -text -noout
    82 |
    83 | 84 | {% if session.authorization.ocsp_subnets %} 85 | {% if certificate.responder_url %} 86 |

    To perform online certificate status request{% if "0.0.0.0/0" not in session.authorization.ocsp_subnets %} 87 | from whitelisted {{ session.authorization.ocsp_subnets }} subnets{% endif %}:

    88 |
    curl http://{{ session.authority.hostname }}/api/certificate > session.pem
     89 | openssl ocsp -issuer session.pem -CAfile session.pem \
     90 |   -url {{ certificate.responder_url }} \
     91 |   -serial 0x{{ certificate.serial }}
    92 | {% else %} 93 |

    Querying OCSP responder disabled for this certificate, see /etc/certidude/profile.conf how to enable if that's desired

    94 | {% endif %} 95 | {% endif %} 96 | 97 |

    To fetch script:

    98 |
    curl --cert-status https://{{ session.authority.hostname }}:8443/api/signed/{{ certificate.common_name }}/script/ \
     99 |     --cacert /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem \
    100 |     --key /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem \
    101 |     --cert /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem
    102 | 103 |
    104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | {% if certificate.lease %} 112 | 115 | {% endif %} 116 | 117 | 121 | 122 | {% if certificate.key_usage %} 123 | 124 | {% endif %} 125 | {% if certificate.extended_key_usage %} 126 | 127 | {% endif %} 128 | 129 |
    Common name{{ certificate.common_name }}
    Organizational unit{% if certificate.organizational_unit %}{{ certificate.organizational_unit }}{% else %}-{% endif %}
    Serial number{{ certificate.serial | serial }}
    Signed{{ certificate.signed | datetime }}{% if certificate.signer %} by {{ certificate.signer }}{% endif %}
    Expires{{ certificate.expires | datetime }}
    Lease{{ certificate.lease.inner_address }} at {{ certificate.lease.last_seen | datetime }} 113 | from {{ certificate.lease.outer_address }} 114 |
    SHA256{{ certificate.sha256sum }}
    Key usage{{ certificate.key_usage | join(", ") | replace("_", " ") }}
    Extended key usage{{ certificate.extended_key_usage | join(", ") | replace("_", " ") }}
    130 |
    131 |
    132 |
    133 |
    134 | -------------------------------------------------------------------------------- /certidude/templates/views/tags.html: -------------------------------------------------------------------------------- 1 | {% for tag in certificate.tags %} 2 | {{ tag.value }} 6 | {% endfor %} 7 | -------------------------------------------------------------------------------- /certidude/templates/views/token.html: -------------------------------------------------------------------------------- 1 |
  • 2 | 3 | 4 | {{ token.uuid }}... 5 | {{ token.subject }} 6 | {% if token.issuer %}{% if token.issuer != token.subject %}by {{ token.issuer }}{% else %}by himself{% endif %}{% else %}via shell{% endif %}, 7 | expires 8 | 9 | 10 |
  • 11 | -------------------------------------------------------------------------------- /certidude/tokens.py: -------------------------------------------------------------------------------- 1 | 2 | import string 3 | from datetime import datetime, timedelta 4 | from certidude import authority, config, mailer, const 5 | from certidude.relational import RelationalMixin 6 | from certidude.common import random 7 | 8 | class TokenManager(RelationalMixin): 9 | SQL_CREATE_TABLES = "token_tables.sql" 10 | 11 | def consume(self, uuid): 12 | now = datetime.utcnow() 13 | retval = self.get( 14 | "select subject, mail, created, expires, profile from token where uuid = ? and created <= ? and ? <= expires and used is null", 15 | uuid, 16 | now + const.CLOCK_SKEW_TOLERANCE, 17 | now - const.CLOCK_SKEW_TOLERANCE) 18 | self.execute( 19 | "update token set used = ? where uuid = ?", 20 | now, 21 | uuid) 22 | return retval 23 | 24 | def issue(self, issuer, subject, subject_mail=None): 25 | # Expand variables 26 | subject_username = subject.name 27 | if not subject_mail: 28 | subject_mail = subject.mail 29 | 30 | # Generate token 31 | token = ''.join(random.choice(string.ascii_lowercase + string.ascii_uppercase + string.digits) for _ in range(32)) 32 | token_created = datetime.utcnow() 33 | token_expires = token_created + config.TOKEN_LIFETIME 34 | 35 | self.sql_execute("token_issue.sql", 36 | token_created, token_expires, token, 37 | issuer.name if issuer else None, 38 | subject_username, subject_mail, "rw") 39 | 40 | # Token lifetime in local time, to select timezone: dpkg-reconfigure tzdata 41 | try: 42 | with open("/etc/timezone") as fh: 43 | token_timezone = fh.read().strip() 44 | except EnvironmentError: 45 | token_timezone = None 46 | 47 | router = sorted([j[0] for j in authority.list_signed( 48 | common_name=config.SERVICE_ROUTERS)])[0] 49 | protocols = ",".join(config.SERVICE_PROTOCOLS) 50 | url = config.TOKEN_URL % locals() 51 | 52 | context = globals() 53 | context.update(locals()) 54 | 55 | mailer.send("token.md", to=subject_mail, **context) 56 | return token 57 | 58 | def list(self, expired=False, used=False): 59 | stmt = "select created as 'created[timestamp]', expires as 'expires[timestamp]', used as 'used[timestamp]', issuer, mail, subject, substr(uuid, 0, 8) as uuid from token" 60 | where = [] 61 | args = [] 62 | if not expired: 63 | where.append(" expires > ?") 64 | args.append(datetime.utcnow()) 65 | if not used: 66 | where.append(" used is null") 67 | if where: 68 | stmt = stmt + " where " + (" and ".join(where)) 69 | stmt += " order by expires" 70 | return self.iterfetch(stmt, *args) 71 | 72 | def purge(self, all=False): 73 | stmt = "delete from token" 74 | args = [] 75 | if not all: 76 | stmt += " where expires < ?" 77 | args.append(datetime.utcnow()) 78 | return self.execute(stmt, *args) 79 | -------------------------------------------------------------------------------- /certidude/user.py: -------------------------------------------------------------------------------- 1 | 2 | import click 3 | import grp 4 | import os 5 | import pwd 6 | from certidude import const, config 7 | 8 | class User(object): 9 | def __init__(self, username, mail, given_name="", surname=""): 10 | self.name = username 11 | self.mail = mail 12 | self.given_name = given_name 13 | self.surname = surname 14 | 15 | def format(self): 16 | if self.given_name or self.surname: 17 | return " ".join([j for j in [self.given_name, self.surname] if j]), "<%s>" % self.mail 18 | else: 19 | return None, self.mail 20 | 21 | def __repr__(self): 22 | return " ".join([j for j in self.format() if j]) 23 | 24 | def __hash__(self): 25 | return hash(self.mail) 26 | 27 | def __eq__(self, other): 28 | if other == None: 29 | return False 30 | assert isinstance(other, User), "%s is not instance of User" % repr(other) 31 | return self.mail == other.mail 32 | 33 | def is_admin(self): 34 | if not hasattr(self, "_is_admin"): 35 | self._is_admin = self.objects.is_admin(self) 36 | return self._is_admin 37 | 38 | class DoesNotExist(Exception): 39 | pass 40 | 41 | 42 | class PosixUserManager(object): 43 | def get(self, username): 44 | _, _, _, _, gecos, _, _ = pwd.getpwnam(username) 45 | gecos = gecos.split(",") 46 | full_name = gecos[0] 47 | mail = "%s@%s" % (username, config.MAIL_SUFFIX) 48 | if full_name and " " in full_name: 49 | given_name, surname = full_name.split(" ", 1) 50 | return User(username, mail, given_name, surname) 51 | return User(username, mail) 52 | 53 | def filter_admins(self): 54 | _, _, gid, members = grp.getgrnam(config.ADMIN_GROUP) 55 | for username in members: 56 | yield self.get(username) 57 | 58 | def is_admin(self, user): 59 | import grp 60 | _, _, gid, members = grp.getgrnam(config.ADMIN_GROUP) 61 | return user.name in members 62 | 63 | def all(self): 64 | _, _, gid, members = grp.getgrnam(config.USERS_GROUP) 65 | for username in members: 66 | yield self.get(username) 67 | for user in self.filter_admins(): # TODO: dedup 68 | yield user 69 | 70 | 71 | class DirectoryConnection(object): 72 | def __enter__(self): 73 | import ldap 74 | import ldap.sasl 75 | 76 | # TODO: Implement simple bind 77 | if not os.path.exists(config.LDAP_GSSAPI_CRED_CACHE): 78 | raise ValueError("Ticket cache at %s not initialized, unable to " 79 | "authenticate with computer account against LDAP server!" % config.LDAP_GSSAPI_CRED_CACHE) 80 | os.environ["KRB5CCNAME"] = config.LDAP_GSSAPI_CRED_CACHE 81 | self.conn = ldap.initialize(config.LDAP_ACCOUNTS_URI, bytes_mode=False) 82 | self.conn.set_option(ldap.OPT_REFERRALS, 0) 83 | click.echo("Connecing to %s using Kerberos ticket cache from %s" % 84 | (config.LDAP_ACCOUNTS_URI, config.LDAP_GSSAPI_CRED_CACHE)) 85 | self.conn.sasl_interactive_bind_s('', ldap.sasl.gssapi()) 86 | return self.conn 87 | 88 | def __exit__(self, type, value, traceback): 89 | self.conn.unbind_s() 90 | del os.environ["KRB5CCNAME"] # prevent contaminating environment 91 | 92 | 93 | class ActiveDirectoryUserManager(object): 94 | def get(self, username): 95 | # TODO: Sanitize username 96 | with DirectoryConnection() as conn: 97 | ft = config.LDAP_USER_FILTER % username 98 | attribs = "cn", "givenName", "sn", config.LDAP_MAIL_ATTRIBUTE, "userPrincipalName" 99 | r = conn.search_s(config.LDAP_BASE, 2, ft, attribs) 100 | for dn, entry in r: 101 | if not dn: 102 | continue 103 | if entry.get("givenname") and entry.get("sn"): 104 | given_name, = entry.get("givenName") 105 | surname, = entry.get("sn") 106 | else: 107 | cn, = entry.get("cn") 108 | if b" " in cn: 109 | given_name, surname = cn.split(b" ", 1) 110 | else: 111 | given_name, surname = cn, b"" 112 | 113 | mail, = entry.get(config.LDAP_MAIL_ATTRIBUTE) or ((username + "@" + const.DOMAIN).encode("ascii"),) 114 | return User(username, mail.decode("ascii"), 115 | given_name.decode("utf-8"), surname.decode("utf-8")) 116 | raise User.DoesNotExist("User %s does not exist" % username) 117 | 118 | def filter(self, ft): 119 | with DirectoryConnection() as conn: 120 | attribs = "givenName", "surname", "samaccountname", "cn", config.LDAP_MAIL_ATTRIBUTE, "userPrincipalName" 121 | r = conn.search_s(config.LDAP_BASE, 2, ft, attribs) 122 | for dn,entry in r: 123 | if not dn: 124 | continue 125 | username, = entry.get("sAMAccountName") 126 | cn, = entry.get("cn") 127 | mail, = entry.get(config.LDAP_MAIL_ATTRIBUTE) or entry.get("userPrincipalName") or (username + b"@" + const.DOMAIN.encode("ascii"),) 128 | if entry.get("givenName") and entry.get("sn"): 129 | given_name, = entry.get("givenName") 130 | surname, = entry.get("sn") 131 | else: 132 | cn, = entry.get("cn") 133 | if b" " in cn: 134 | given_name, surname = cn.split(b" ", 1) 135 | else: 136 | given_name, surname = cn, b"" 137 | yield User(username.decode("utf-8"), mail.decode("utf-8"), 138 | given_name.decode("utf-8"), surname.decode("utf-8")) 139 | 140 | def filter_admins(self): 141 | """ 142 | Return admin User objects 143 | """ 144 | return self.filter(config.LDAP_ADMIN_FILTER % "*") 145 | 146 | def all(self): 147 | """ 148 | Return all valid User objects 149 | """ 150 | return self.filter(ft=config.LDAP_USER_FILTER % "*") 151 | 152 | def is_admin(self, user): 153 | with DirectoryConnection() as conn: 154 | ft = config.LDAP_ADMIN_FILTER % user.name 155 | r = conn.search_s(config.LDAP_BASE, 2, ft, ["cn"]) 156 | for dn, entry in r: 157 | if not dn: 158 | continue 159 | return True 160 | return False 161 | 162 | if config.ACCOUNTS_BACKEND == "ldap": 163 | User.objects = ActiveDirectoryUserManager() 164 | elif config.ACCOUNTS_BACKEND == "posix": 165 | User.objects = PosixUserManager() 166 | else: 167 | raise NotImplementedError("Authorization backend %s not supported" % repr(config.AUTHORIZATION_BACKEND)) 168 | 169 | -------------------------------------------------------------------------------- /doc/certidude.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurivosandi/certidude/6e50c85c8587d3e800d01ce1d1588965c29b2c46/doc/certidude.png -------------------------------------------------------------------------------- /doc/openwrt.md: -------------------------------------------------------------------------------- 1 | # OpenWrt/LEDE integration guide 2 | 3 | ## Software dependencies 4 | 5 | On vanilla OpenWrt/LEDE box install software packages: 6 | 7 | ```bash 8 | opkg update 9 | opkg install curl openssl-util 10 | opkg install strongswan-full kmod-crypto-echainiv kmod-crypto-gcm 11 | ``` 12 | 13 | When using image builder specify these packages via PACKAGES environment variable. 14 | 15 | Grab 50-certidude script and place it to /etc/hotplug.d/iface/50-certidude: 16 | 17 | ```bash 18 | wget https://raw.githubusercontent.com/laurivosandi/certidude/master/doc/50-certidude -O /etc/hotplug.d/iface/50-certidude 19 | ``` 20 | 21 | ## As IPSec gateway 22 | 23 | Configure /etc/ipsec.conf: 24 | 25 | ``` 26 | config setup 27 | cachecrls=yes 28 | strictcrlpolicy=yes 29 | 30 | ca ca2 31 | auto = add 32 | cacert = /etc/config/ca.crt 33 | ocspuri = http://ca.example.com/api/ocsp/ 34 | 35 | conn %default 36 | ikelifetime=60m 37 | keylife=20m 38 | rekeymargin=3m 39 | keyingtries=1 40 | keyexchange=ikev2 41 | 42 | conn site-to-client 43 | auto=add 44 | right=%any # Allow connecting from any IP address 45 | rightsourceip=10.179.44.0/24 # Serve virtual IP-s from this pool 46 | left=router.example.com # Gateway FQDN 47 | leftcert=/etc/config/robo-router.crt # Gateway certificate 48 | leftupdown=/usr/bin/certidude-updown 49 | leftsubnet=192.168.12.0/24,10.179.0.0/16 # Push routes 50 | rightdns=192.168.12.1 # Push DNS server to clients 51 | ``` 52 | 53 | When you want to make DNS queries possible via tunnel don't forget to 54 | disable local service for dnsmasq: 55 | 56 | ```bash 57 | uci set dhcp.@dnsmasq[0].localservice=0 58 | uci commit 59 | ``` 60 | 61 | Place following to /usr/bin/certidude-updown, when tunnel goes up submit lease to CA: 62 | 63 | ```bash 64 | #!/bin/sh 65 | 66 | case $PLUTO_VERB in 67 | up-client) 68 | curl -f --data "outer_address=$PLUTO_PEER&inner_address=$PLUTO_PEER_SOURCEIP&client=$(echo $PLUTO_PEER_ID | cut -d '=' -f 2)" \ 69 | http://ca.example.com/api/lease/ 70 | ;; 71 | *) 72 | curl -f -X POST -d "client=$X509_0_CN&server=$X509_1_CN&outer_address=$untrusted_ip&inner_address=$ifconfig_pool_remote_ip&serial=$tls_serial_0" \ 73 | http://ca.example.com/api/lease/ 74 | ;; 75 | esac 76 | ``` 77 | 78 | 79 | ## As client 80 | 81 | Grab 50-certidude script and place it to /etc/hotplug.d/iface/ as shown above. 82 | 83 | Place following to /etc/ipsec.conf: 84 | 85 | ``` 86 | config setup 87 | 88 | conn %default 89 | keyexchange=ikev2 90 | keyingtries=300 91 | dpdaction=restart 92 | closeaction=restart 93 | 94 | conn client-to-site 95 | auto=add 96 | leftupdown=/usr/bin/ipsec-updown 97 | left=%defaultroute 98 | leftsourceip=%config 99 | leftcert=/etc/ipsec.d/certs/client.pem 100 | right=router.example.com 101 | rightsubnet=0.0.0.0/0 102 | ``` 103 | 104 | Scripting client, when tunnel goes up: 105 | 106 | ```bash 107 | #!/bin/sh 108 | [ "$PLUTO_VERB" != "up-client" ] && exit 0 109 | 110 | case "$PLUTO_PEER_CLIENT" in 111 | 192.*|172.*|10.*) 112 | # Do nothing 113 | exit 0 114 | ;; 115 | *) 116 | # Attempt to fetch script from server 117 | logger -t certidude -s "IPsec SA to $PLUTO_PEER_CLIENT established, attempting to fetch script" 118 | SCRIPT=$(mktemp -u) 119 | wget --header='Accept: text/x-shellscript' http://ca.example.com/api/script -O $SCRIPT 120 | sh $SCRIPT 121 | ;; 122 | esac 123 | ``` 124 | at /etc/config/certidude you can use: 125 | 126 | ``` 127 | config authority 128 | option url http://ca.example.com 129 | option authority_path /etc/ipsec.d/cacerts/ca.pem 130 | option request_path /etc/ipsec.d/reqs/client.pem 131 | option certificate_path /etc/ipsec.d/certs/client.pem 132 | option key_path /etc/ipsec.d/private/client.pem 133 | option key_type rsa 134 | option key_length 1024 135 | option red_led gl-connect:red:wlan 136 | option green_led gl-connect:green:lan 137 | ``` 138 | 139 | To test: 140 | 141 | ```bash 142 | ACTION=ifup INTERFACE=wan sh /etc/hotplug.d/iface/50-certidude 143 | ``` 144 | 145 | # As site-to-site router 146 | 147 | In this example Omnia Turris is set up as a router which enables 148 | access to a subnet behind another IPSec gateway. 149 | 150 | Set up /etc/config/certidude: 151 | 152 | ```bash 153 | config authority ca 154 | option key_type rsa 155 | option key_length 1024 156 | option url http://ca.example.com 157 | option common_name turris-123456 158 | option key_path /etc/ipsec.d/private/router.pem 159 | option request_path /etc/ipsec.d/reqs/router.pem 160 | option certificate_path /etc/ipsec.d/certs/router.pem 161 | option authority_path /etc/ipsec.d/cacerts/ca.pem 162 | option revocations_path /etc/ipsec.d/crls/router.pem 163 | option red_led omnia-led:user1 164 | option green_led omnia-led:user2 165 | ``` 166 | 167 | Set up /etc/ipsec.conf: 168 | 169 | ``` 170 | config setup 171 | cachecrls=yes 172 | strictcrlpolicy=yes 173 | 174 | conn s2s 175 | auto=start 176 | right=router.example.com 177 | leftcert=/etc/ipsec.d/certs/router.pem 178 | leftsubnet=172.26.1.0/24 # local subnet 179 | rightsubnet=172.24.0.0/24 # subnet behind gateway 180 | ``` 181 | 182 | Reconfigure firewall: 183 | 184 | ```bash 185 | # Prevent NAT-ing of IPSec tunnel packets 186 | iptables -t nat -I POSTROUTING -m policy --dir out --pol ipsec -j ACCEPT 187 | 188 | # Trust packets from IPSec tunnel 189 | iptables -I INPUT -m policy --dir in --pol ipsec -j ACCEPT 190 | iptables -I FORWARD -m policy --dir in --pol ipsec -j ACCEPT 191 | ``` 192 | 193 | DNS forwarding and caching: 194 | 195 | ```bash 196 | uci delete dhcp.@dnsmasq[0].local 197 | uci set dhcp.@dnsmasq[0].domain=example.lan 198 | uci add_list dhcp.@dnsmasq[0].server="/.example.lan/172.24.1.1" 199 | uci add_list dhcp.@dnsmasq[0].rebind_domain="example.lan" 200 | uci commit 201 | ``` 202 | 203 | On Omnia turris kresd is used instead of dnsmasq, to revert back to dnsmasq: 204 | 205 | ```bash 206 | /etc/init.d/kresd stop 207 | /etc/init.d/kresd disable 208 | uci del_list dhcp.lan.dhcp_option="6,192.168.1.1" 209 | uci delete dhcp.@dnsmasq[0].port 210 | uci commit 211 | /etc/init.d/dnsmasq enable 212 | /etc/init.d/dnsmasq restart 213 | ``` 214 | 215 | To disable IPv6: 216 | 217 | ```bash 218 | /etc/init.d/odhcpd stop 219 | /etc/init.d/odhcpd disable 220 | ``` 221 | 222 | -------------------------------------------------------------------------------- /doc/strongswan-updown.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cat << EOF | curl -s -X POST -d @- -H "X-EventSource-Event: $PLUTO_VERB" http://ca.example.com/pub/?id=CA-channel-identifier-goes-here 3 | {"address": "$(echo $PLUTO_PEER_CLIENT | sed 's/\/32$//')", "peer": "$PLUTO_PEER", "identity": "$PLUTO_PEER_ID", "routed_subnet": "$PLUTO_MY_CLIENT"} 4 | EOF 5 | -------------------------------------------------------------------------------- /doc/usecase-diagram.dia: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurivosandi/certidude/6e50c85c8587d3e800d01ce1d1588965c29b2c46/doc/usecase-diagram.dia -------------------------------------------------------------------------------- /doc/usecase-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laurivosandi/certidude/6e50c85c8587d3e800d01ce1d1588965c29b2c46/doc/usecase-diagram.png -------------------------------------------------------------------------------- /misc/certidude: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from certidude.cli import entry_point 4 | 5 | if __name__ == "__main__": 6 | entry_point() 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click>=6.7 2 | configparser>=3.5.0 3 | certbuilder 4 | crlbuilder 5 | csrbuilder 6 | oscrypto 7 | requests 8 | jinja2 9 | ipsecparse 10 | setproctitle 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf-8 3 | import os 4 | from setuptools import setup 5 | 6 | setup( 7 | name = "certidude", 8 | version = "0.1.21", 9 | author = u"Lauri Võsandi", 10 | author_email = "lauri.vosandi@gmail.com", 11 | description = "Certidude is a novel X.509 Certificate Authority management tool aiming to support PKCS#11 and in far future WebCrypto.", 12 | license = "MIT", 13 | keywords = "falcon http jinja2 x509 pkcs11 webcrypto kerberos ldap", 14 | url = "http://github.com/laurivosandi/certidude", 15 | packages=[ 16 | "certidude", 17 | "certidude.api", 18 | "certidude.api.utils" 19 | ], 20 | long_description=open("README.rst").read(), 21 | # Include here only stuff required to run certidude client 22 | install_requires=[ 23 | "asn1crypto", 24 | "click", 25 | "configparser", 26 | "certbuilder", 27 | "csrbuilder", 28 | "crlbuilder", 29 | "jinja2", 30 | ], 31 | scripts=[ 32 | "misc/certidude" 33 | ], 34 | include_package_data = True, 35 | package_data={ 36 | "certidude": ["certidude/templates/*", "certidude/static/*", "certidude/builder/*"], 37 | }, 38 | classifiers=[ 39 | "Development Status :: 4 - Beta", 40 | "Environment :: Console", 41 | "Intended Audience :: Developers", 42 | "Intended Audience :: System Administrators", 43 | "License :: Freely Distributable", 44 | "License :: OSI Approved :: MIT License", 45 | "Natural Language :: English", 46 | "Operating System :: POSIX :: Linux", 47 | "Programming Language :: Python", 48 | "Programming Language :: Python :: 3 :: Only", 49 | ], 50 | ) 51 | 52 | --------------------------------------------------------------------------------