mirror of
				https://github.com/laurivosandi/certidude
				synced 2025-10-31 01:19:11 +00:00 
			
		
		
		
	Migrate signature profiles to separate config file
This commit is contained in:
		| @@ -163,7 +163,8 @@ class SessionResource(AuthorityHandler): | |||||||
|                 admin_subnets=config.ADMIN_SUBNETS or None, |                 admin_subnets=config.ADMIN_SUBNETS or None, | ||||||
|                 signature = dict( |                 signature = dict( | ||||||
|                     revocation_list_lifetime=config.REVOCATION_LIST_LIFETIME, |                     revocation_list_lifetime=config.REVOCATION_LIST_LIFETIME, | ||||||
|                     profiles = [dict(name=k, server=v[0]=="server", lifetime=v[1], organizational_unit=v[2], title=v[3]) for k,v in config.PROFILES.items()] |                     profiles = sorted([p.serialize() for p in config.PROFILES.values()], key=lambda p:p.get("slug")), | ||||||
|  |  | ||||||
|                 ) |                 ) | ||||||
|             ) if req.context.get("user").is_admin() else None, |             ) if req.context.get("user").is_admin() else None, | ||||||
|             features=dict( |             features=dict( | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ from base64 import b64decode | |||||||
| from certidude import config, push, errors | from certidude import config, push, errors | ||||||
| from certidude.auth import login_required, login_optional, authorize_admin | from certidude.auth import login_required, login_optional, authorize_admin | ||||||
| from certidude.decorators import csrf_protection, MyEncoder | from certidude.decorators import csrf_protection, MyEncoder | ||||||
|  | from certidude.profile import SignatureProfile | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from oscrypto import asymmetric | from oscrypto import asymmetric | ||||||
| from oscrypto.errors import SignatureError | from oscrypto.errors import SignatureError | ||||||
| @@ -71,7 +72,8 @@ class RequestListResource(AuthorityHandler): | |||||||
|  |  | ||||||
|                 # Automatic enroll with Kerberos machine cerdentials |                 # Automatic enroll with Kerberos machine cerdentials | ||||||
|                 resp.set_header("Content-Type", "application/x-pem-file") |                 resp.set_header("Content-Type", "application/x-pem-file") | ||||||
|                 cert, resp.body = self.authority._sign(csr, body, overwrite=True) |                 cert, resp.body = self.authority._sign(csr, body, | ||||||
|  |                     profile=config.PROFILES["rw"], overwrite=True) | ||||||
|                 logger.info("Automatically enrolled Kerberos authenticated machine %s from %s", |                 logger.info("Automatically enrolled Kerberos authenticated machine %s from %s", | ||||||
|                     machine, req.context.get("remote_addr")) |                     machine, req.context.get("remote_addr")) | ||||||
|                 return |                 return | ||||||
| @@ -89,21 +91,19 @@ class RequestListResource(AuthorityHandler): | |||||||
|             cert_pk = cert["tbs_certificate"]["subject_public_key_info"].native |             cert_pk = cert["tbs_certificate"]["subject_public_key_info"].native | ||||||
|             csr_pk = csr["certification_request_info"]["subject_pk_info"].native |             csr_pk = csr["certification_request_info"]["subject_pk_info"].native | ||||||
|  |  | ||||||
|             try: |  | ||||||
|                 buf = req.get_header("X-SSL-CERT") |  | ||||||
|                 header, _, der_bytes = pem.unarmor(buf.replace("\t", "").encode("ascii")) |  | ||||||
|                 handshake_cert = x509.Certificate.load(der_bytes) |  | ||||||
|             except: |  | ||||||
|                 raise |  | ||||||
|             else: |  | ||||||
|             # Same public key |             # Same public key | ||||||
|             if cert_pk == csr_pk: |             if cert_pk == csr_pk: | ||||||
|  |                 buf = req.get_header("X-SSL-CERT") | ||||||
|                 # Used mutually authenticated TLS handshake, assume renewal |                 # Used mutually authenticated TLS handshake, assume renewal | ||||||
|  |                 if buf: | ||||||
|  |                     header, _, der_bytes = pem.unarmor(buf.replace("\t", "").encode("ascii")) | ||||||
|  |                     handshake_cert = x509.Certificate.load(der_bytes) | ||||||
|                     if handshake_cert.native == cert.native: |                     if handshake_cert.native == cert.native: | ||||||
|                         for subnet in config.RENEWAL_SUBNETS: |                         for subnet in config.RENEWAL_SUBNETS: | ||||||
|                             if req.context.get("remote_addr") in subnet: |                             if req.context.get("remote_addr") in subnet: | ||||||
|                                 resp.set_header("Content-Type", "application/x-x509-user-cert") |                                 resp.set_header("Content-Type", "application/x-x509-user-cert") | ||||||
|                                 _, resp.body = self.authority._sign(csr, body, overwrite=True) |                                 _, resp.body = self.authority._sign(csr, body, overwrite=True, | ||||||
|  |                                     profile=SignatureProfile.from_cert(cert)) | ||||||
|                                 logger.info("Renewing certificate for %s as %s is whitelisted", common_name, req.context.get("remote_addr")) |                                 logger.info("Renewing certificate for %s as %s is whitelisted", common_name, req.context.get("remote_addr")) | ||||||
|                                 return |                                 return | ||||||
|  |  | ||||||
| @@ -123,7 +123,7 @@ class RequestListResource(AuthorityHandler): | |||||||
|                     if req.context.get("remote_addr") in subnet: |                     if req.context.get("remote_addr") in subnet: | ||||||
|                         try: |                         try: | ||||||
|                             resp.set_header("Content-Type", "application/x-pem-file") |                             resp.set_header("Content-Type", "application/x-pem-file") | ||||||
|                             _, resp.body = self.authority._sign(csr, body) |                             _, resp.body = self.authority._sign(csr, body, profile=config.PROFILES["rw"]) | ||||||
|                             logger.info("Autosigned %s as %s is whitelisted", common_name, req.context.get("remote_addr")) |                             logger.info("Autosigned %s as %s is whitelisted", common_name, req.context.get("remote_addr")) | ||||||
|                             return |                             return | ||||||
|                         except EnvironmentError: |                         except EnvironmentError: | ||||||
| @@ -222,7 +222,7 @@ class RequestDetailResource(AuthorityHandler): | |||||||
|         """ |         """ | ||||||
|         try: |         try: | ||||||
|             cert, buf = self.authority.sign(cn, |             cert, buf = self.authority.sign(cn, | ||||||
|                 profile=req.get_param("profile", default="default"), |                 profile=config.PROFILES[req.get_param("profile", default="rw")], | ||||||
|                 overwrite=True, |                 overwrite=True, | ||||||
|                 signer=req.context.get("user").name) |                 signer=req.context.get("user").name) | ||||||
|             # Mailing and long poll publishing implemented in the function above |             # Mailing and long poll publishing implemented in the function above | ||||||
|   | |||||||
| @@ -71,7 +71,7 @@ def self_enroll(): | |||||||
|         from certidude import authority |         from certidude import authority | ||||||
|         from certidude.common import drop_privileges |         from certidude.common import drop_privileges | ||||||
|         drop_privileges() |         drop_privileges() | ||||||
|         authority.sign(common_name, skip_push=True, overwrite=True, profile="srv") |         authority.sign(common_name, skip_push=True, overwrite=True, profile=config.PROFILES["srv"]) | ||||||
|         sys.exit(0) |         sys.exit(0) | ||||||
|     else: |     else: | ||||||
|         os.waitpid(pid, 0) |         os.waitpid(pid, 0) | ||||||
| @@ -82,7 +82,7 @@ def self_enroll(): | |||||||
|  |  | ||||||
|  |  | ||||||
| def get_request(common_name): | def get_request(common_name): | ||||||
|     if not re.match(const.RE_HOSTNAME, common_name): |     if not re.match(const.RE_COMMON_NAME, common_name): | ||||||
|         raise ValueError("Invalid common name %s" % repr(common_name)) |         raise ValueError("Invalid common name %s" % repr(common_name)) | ||||||
|     path = os.path.join(config.REQUESTS_DIR, common_name + ".pem") |     path = os.path.join(config.REQUESTS_DIR, common_name + ".pem") | ||||||
|     try: |     try: | ||||||
| @@ -95,7 +95,7 @@ def get_request(common_name): | |||||||
|         raise errors.RequestDoesNotExist("Certificate signing request file %s does not exist" % path) |         raise errors.RequestDoesNotExist("Certificate signing request file %s does not exist" % path) | ||||||
|  |  | ||||||
| def get_signed(common_name): | def get_signed(common_name): | ||||||
|     if not re.match(const.RE_HOSTNAME, common_name): |     if not re.match(const.RE_COMMON_NAME, common_name): | ||||||
|         raise ValueError("Invalid common name %s" % repr(common_name)) |         raise ValueError("Invalid common name %s" % repr(common_name)) | ||||||
|     path = os.path.join(config.SIGNED_DIR, common_name + ".pem") |     path = os.path.join(config.SIGNED_DIR, common_name + ".pem") | ||||||
|     with open(path, "rb") as fh: |     with open(path, "rb") as fh: | ||||||
| @@ -158,7 +158,7 @@ def store_request(buf, overwrite=False, address="", user=""): | |||||||
|  |  | ||||||
|     common_name = csr["certification_request_info"]["subject"].native["common_name"] |     common_name = csr["certification_request_info"]["subject"].native["common_name"] | ||||||
|  |  | ||||||
|     if not re.match(const.RE_HOSTNAME, common_name): |     if not re.match(const.RE_COMMON_NAME, common_name): | ||||||
|         raise ValueError("Invalid common name") |         raise ValueError("Invalid common name") | ||||||
|  |  | ||||||
|     request_path = os.path.join(config.REQUESTS_DIR, common_name + ".pem") |     request_path = os.path.join(config.REQUESTS_DIR, common_name + ".pem") | ||||||
| @@ -296,7 +296,7 @@ def export_crl(pem=True): | |||||||
|  |  | ||||||
| def delete_request(common_name): | def delete_request(common_name): | ||||||
|     # Validate CN |     # Validate CN | ||||||
|     if not re.match(const.RE_HOSTNAME, common_name): |     if not re.match(const.RE_COMMON_NAME, common_name): | ||||||
|         raise ValueError("Invalid common name") |         raise ValueError("Invalid common name") | ||||||
|  |  | ||||||
|     path, buf, csr, submitted = get_request(common_name) |     path, buf, csr, submitted = get_request(common_name) | ||||||
| @@ -310,7 +310,7 @@ def delete_request(common_name): | |||||||
|         config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest(), |         config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest(), | ||||||
|         headers={"User-Agent": "Certidude API"}) |         headers={"User-Agent": "Certidude API"}) | ||||||
|  |  | ||||||
| def sign(common_name, skip_notify=False, skip_push=False, overwrite=False, profile="default", signer=None): | def sign(common_name, profile, skip_notify=False, skip_push=False, overwrite=False, signer=None): | ||||||
|     """ |     """ | ||||||
|     Sign certificate signing request by it's common name |     Sign certificate signing request by it's common name | ||||||
|     """ |     """ | ||||||
| @@ -323,16 +323,13 @@ def sign(common_name, skip_notify=False, skip_push=False, overwrite=False, profi | |||||||
|  |  | ||||||
|  |  | ||||||
|     # Sign with function below |     # Sign with function below | ||||||
|     cert, buf = _sign(csr, csr_buf, skip_notify, skip_push, overwrite, profile, signer) |     cert, buf = _sign(csr, csr_buf, profile, skip_notify, skip_push, overwrite, signer) | ||||||
|  |  | ||||||
|     os.unlink(req_path) |     os.unlink(req_path) | ||||||
|     return cert, buf |     return cert, buf | ||||||
|  |  | ||||||
| def _sign(csr, buf, skip_notify=False, skip_push=False, overwrite=False, profile="default", signer=None): | def _sign(csr, buf, profile, skip_notify=False, skip_push=False, overwrite=False, signer=None): | ||||||
|     # TODO: CRLDistributionPoints, OCSP URL, Certificate URL |     # TODO: CRLDistributionPoints, OCSP URL, Certificate URL | ||||||
|     if profile not in config.PROFILES: |  | ||||||
|         raise ValueError("Invalid profile supplied '%s'" % profile) |  | ||||||
|  |  | ||||||
|     assert buf.startswith(b"-----BEGIN ") |     assert buf.startswith(b"-----BEGIN ") | ||||||
|     assert isinstance(csr, CertificationRequest) |     assert isinstance(csr, CertificationRequest) | ||||||
|     csr_pubkey = asymmetric.load_public_key(csr["certification_request_info"]["subject_pk_info"]) |     csr_pubkey = asymmetric.load_public_key(csr["certification_request_info"]["subject_pk_info"]) | ||||||
| @@ -370,10 +367,9 @@ def _sign(csr, buf, skip_notify=False, skip_push=False, overwrite=False, profile | |||||||
|         else: |         else: | ||||||
|             raise FileExistsError("Will not overwrite existing certificate") |             raise FileExistsError("Will not overwrite existing certificate") | ||||||
|  |  | ||||||
|     # Sign via signer process |  | ||||||
|     dn = {u'common_name': common_name } |     dn = {u'common_name': common_name } | ||||||
|     profile_server_flags, lifetime, dn["organizational_unit_name"], _ = config.PROFILES[profile] |     if profile.ou: | ||||||
|     lifetime = int(lifetime) |         dn["organizational_unit_name"] = profile.ou | ||||||
|  |  | ||||||
|     builder = CertificateBuilder(dn, csr_pubkey) |     builder = CertificateBuilder(dn, csr_pubkey) | ||||||
|     builder.serial_number = random.randint( |     builder.serial_number = random.randint( | ||||||
| @@ -382,18 +378,12 @@ def _sign(csr, buf, skip_notify=False, skip_push=False, overwrite=False, profile | |||||||
|  |  | ||||||
|     now = datetime.utcnow() |     now = datetime.utcnow() | ||||||
|     builder.begin_date = now - timedelta(minutes=5) |     builder.begin_date = now - timedelta(minutes=5) | ||||||
|     builder.end_date = now + timedelta(days=lifetime) |     builder.end_date = now + timedelta(days=profile.lifetime) | ||||||
|     builder.issuer = certificate |     builder.issuer = certificate | ||||||
|     builder.ca = False |     builder.ca = profile.ca | ||||||
|     builder.key_usage = set(["digital_signature", "key_encipherment"]) |     builder.key_usage = profile.key_usage | ||||||
|  |     builder.extended_key_usage = profile.extended_key_usage | ||||||
|     # If we have FQDN and profile suggests server flags, enable them |     builder.subject_alt_domains = [common_name] | ||||||
|     if server_flags(common_name) and profile_server_flags: |  | ||||||
|         builder.subject_alt_domains = [common_name] # OpenVPN uses CN while StrongSwan uses SAN to match hostname of the server |  | ||||||
|         builder.extended_key_usage = set(["server_auth", "1.3.6.1.5.5.8.2.2", "client_auth"]) |  | ||||||
|     else: |  | ||||||
|         builder.subject_alt_domains = [common_name] # iOS demands SAN also for clients |  | ||||||
|         builder.extended_key_usage = set(["client_auth"]) |  | ||||||
|  |  | ||||||
|     end_entity_cert = builder.build(private_key) |     end_entity_cert = builder.build(private_key) | ||||||
|     end_entity_cert_buf = asymmetric.dump_certificate(end_entity_cert) |     end_entity_cert_buf = asymmetric.dump_certificate(end_entity_cert) | ||||||
|   | |||||||
| @@ -281,8 +281,8 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self): | |||||||
|             common_name = const.FQDN |             common_name = const.FQDN | ||||||
|         elif "$" in common_name: |         elif "$" in common_name: | ||||||
|             raise ValueError("Invalid variable '%s' supplied, only $HOSTNAME and $FQDN allowed" % common_name) |             raise ValueError("Invalid variable '%s' supplied, only $HOSTNAME and $FQDN allowed" % common_name) | ||||||
|         if not re.match(const.RE_HOSTNAME, common_name): |         if not re.match(const.RE_COMMON_NAME, common_name): | ||||||
|             raise ValueError("Invalid common name '%s' supplied" % common_name) |             raise ValueError("Supplied common name %s doesn't match the expression %s" % (common_name, const.RE_COMMON_NAME)) | ||||||
|  |  | ||||||
|  |  | ||||||
|         ################################ |         ################################ | ||||||
| @@ -338,7 +338,7 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self): | |||||||
|  |  | ||||||
|         try: |         try: | ||||||
|             renewal_overlap = clients.getint(authority_name, "renewal overlap") |             renewal_overlap = clients.getint(authority_name, "renewal overlap") | ||||||
|         except NoOptionError: # Renewal not specified in config |         except NoOptionError: # Renewal not configured | ||||||
|             renewal_overlap = None |             renewal_overlap = None | ||||||
|  |  | ||||||
|         try: |         try: | ||||||
| @@ -1169,6 +1169,14 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country, | |||||||
|                 fh.write(env.get_template("server/builder.conf").render(vars())) |                 fh.write(env.get_template("server/builder.conf").render(vars())) | ||||||
|             click.echo("File %s created" % const.BUILDER_CONFIG_PATH) |             click.echo("File %s created" % const.BUILDER_CONFIG_PATH) | ||||||
|  |  | ||||||
|  |         # Create image builder config | ||||||
|  |         if os.path.exists(const.PROFILE_CONFIG_PATH): | ||||||
|  |             click.echo("Signature profile config %s already exists, remove to regenerate" % const.PROFILE_CONFIG_PATH) | ||||||
|  |         else: | ||||||
|  |             with open(const.PROFILE_CONFIG_PATH, "w") as fh: | ||||||
|  |                 fh.write(env.get_template("server/profile.conf").render(vars())) | ||||||
|  |             click.echo("File %s created" % const.PROFILE_CONFIG_PATH) | ||||||
|  |  | ||||||
|         # Create directory with 755 permissions |         # Create directory with 755 permissions | ||||||
|         os.umask(0o022) |         os.umask(0o022) | ||||||
|         if not os.path.exists(directory): |         if not os.path.exists(directory): | ||||||
| @@ -1347,12 +1355,12 @@ def certidude_list(verbose, show_key_type, show_extensions, show_path, show_sign | |||||||
|  |  | ||||||
| @click.command("sign", help="Sign certificate") | @click.command("sign", help="Sign certificate") | ||||||
| @click.argument("common_name") | @click.argument("common_name") | ||||||
| @click.option("--profile", "-p", default="default", help="Profile") | @click.option("--profile", "-p", default="rw", help="Profile") | ||||||
| @click.option("--overwrite", "-o", default=False, is_flag=True, help="Revoke valid certificate with same CN") | @click.option("--overwrite", "-o", default=False, is_flag=True, help="Revoke valid certificate with same CN") | ||||||
| def certidude_sign(common_name, overwrite, profile): | def certidude_sign(common_name, overwrite, profile): | ||||||
|     from certidude import authority |     from certidude import authority | ||||||
|     drop_privileges() |     drop_privileges() | ||||||
|     cert = authority.sign(common_name, overwrite=overwrite, profile=profile) |     cert = authority.sign(common_name, overwrite=overwrite, profile=config.PROFILES[profile]) | ||||||
|  |  | ||||||
|  |  | ||||||
| @click.command("revoke", help="Revoke certificate") | @click.command("revoke", help="Revoke certificate") | ||||||
| @@ -1397,6 +1405,11 @@ def certidude_serve(port, listen, fork): | |||||||
|  |  | ||||||
|     from certidude import config |     from certidude import config | ||||||
|  |  | ||||||
|  |     click.echo("Loading signature profiles:") | ||||||
|  |     for profile in config.PROFILES.values(): | ||||||
|  |          click.echo("- %s" % profile) | ||||||
|  |     click.echo() | ||||||
|  |  | ||||||
|     # Rebuild reverse mapping |     # Rebuild reverse mapping | ||||||
|     for cn, path, buf, cert, signed, expires in authority.list_signed(): |     for cn, path, buf, cert, signed, expires in authority.list_signed(): | ||||||
|         by_serial = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial_number) |         by_serial = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial_number) | ||||||
|   | |||||||
| @@ -2,7 +2,9 @@ import configparser | |||||||
| import ipaddress | import ipaddress | ||||||
| import os | import os | ||||||
| from certidude import const | from certidude import const | ||||||
|  | from certidude.profile import SignatureProfile | ||||||
| from collections import OrderedDict | from collections import OrderedDict | ||||||
|  | from datetime import timedelta | ||||||
|  |  | ||||||
| # Options that are parsed from config file are fetched here | # Options that are parsed from config file are fetched here | ||||||
|  |  | ||||||
| @@ -96,11 +98,22 @@ TOKEN_SECRET = cp.get("token", "secret").encode("ascii") | |||||||
| # The API call for looking up scripts uses following directory as root | # The API call for looking up scripts uses following directory as root | ||||||
| SCRIPT_DIR = cp.get("script", "path") | SCRIPT_DIR = cp.get("script", "path") | ||||||
|  |  | ||||||
| PROFILES = OrderedDict([[i, [j.strip() for j in cp.get("profile", i).split(",")]] for i in cp.options("profile")]) | from configparser import ConfigParser | ||||||
|  | profile_config = ConfigParser() | ||||||
|  | profile_config.readfp(open(const.PROFILE_CONFIG_PATH)) | ||||||
|  |  | ||||||
|  | PROFILES = dict([(key, SignatureProfile(key, | ||||||
|  |     profile_config.get(key, "title"), | ||||||
|  |     profile_config.get(key, "ou"), | ||||||
|  |     profile_config.getboolean(key, "ca"), | ||||||
|  |     profile_config.getint(key, "lifetime"), | ||||||
|  |     profile_config.get(key, "key usage"), | ||||||
|  |     profile_config.get(key, "extended key usage"), | ||||||
|  |     profile_config.get(key, "common name"), | ||||||
|  |     )) for key in profile_config.sections()]) | ||||||
|  |  | ||||||
| cp2 = configparser.RawConfigParser() | cp2 = configparser.RawConfigParser() | ||||||
| cp2.readfp(open(const.BUILDER_CONFIG_PATH, "r")) | cp2.readfp(open(const.BUILDER_CONFIG_PATH, "r")) | ||||||
| IMAGE_BUILDER_PROFILES = [(j, cp2.get(j, "title"), cp2.get(j, "rename")) for j in cp2.sections()] | IMAGE_BUILDER_PROFILES = [(j, cp2.get(j, "title"), cp2.get(j, "rename")) for j in cp2.sections()] | ||||||
|  |  | ||||||
|  |  | ||||||
| TOKEN_OVERWRITE_PERMITTED=True | TOKEN_OVERWRITE_PERMITTED=True | ||||||
|   | |||||||
| @@ -6,12 +6,15 @@ import sys | |||||||
|  |  | ||||||
| KEY_SIZE = 1024 if os.getenv("TRAVIS") else 4096 | KEY_SIZE = 1024 if os.getenv("TRAVIS") else 4096 | ||||||
| CURVE_NAME = "secp384r1" | CURVE_NAME = "secp384r1" | ||||||
| RE_HOSTNAME =  "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])(@(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9]))?$" | RE_FQDN =  "^(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)+([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])?$" | ||||||
|  | RE_HOSTNAME =  "^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$" | ||||||
|  | RE_COMMON_NAME = "^[A-Za-z0-9\-\.\@]+$" | ||||||
|  |  | ||||||
| RUN_DIR = "/run/certidude" | RUN_DIR = "/run/certidude" | ||||||
| CONFIG_DIR = "/etc/certidude" | CONFIG_DIR = "/etc/certidude" | ||||||
| SERVER_CONFIG_PATH = os.path.join(CONFIG_DIR, "server.conf") | SERVER_CONFIG_PATH = os.path.join(CONFIG_DIR, "server.conf") | ||||||
| BUILDER_CONFIG_PATH = os.path.join(CONFIG_DIR, "builder.conf") | BUILDER_CONFIG_PATH = os.path.join(CONFIG_DIR, "builder.conf") | ||||||
|  | PROFILE_CONFIG_PATH = os.path.join(CONFIG_DIR, "profile.conf") | ||||||
| CLIENT_CONFIG_PATH = os.path.join(CONFIG_DIR, "client.conf") | CLIENT_CONFIG_PATH = os.path.join(CONFIG_DIR, "client.conf") | ||||||
| SERVICES_CONFIG_PATH = os.path.join(CONFIG_DIR, "services.conf") | SERVICES_CONFIG_PATH = os.path.join(CONFIG_DIR, "services.conf") | ||||||
| SERVER_PID_PATH = os.path.join(RUN_DIR, "server.pid") | SERVER_PID_PATH = os.path.join(RUN_DIR, "server.pid") | ||||||
|   | |||||||
							
								
								
									
										52
									
								
								certidude/profile.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								certidude/profile.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | |||||||
|  |  | ||||||
|  | import click | ||||||
|  | from datetime import timedelta | ||||||
|  | from certidude import const | ||||||
|  |  | ||||||
|  | class SignatureProfile(object): | ||||||
|  |     def __init__(self, slug, title, ou, ca, lifetime, key_usage, extended_key_usage, common_name): | ||||||
|  |         self.slug = slug | ||||||
|  |         self.title = title | ||||||
|  |         self.ou = ou or None | ||||||
|  |         self.ca = ca | ||||||
|  |         self.lifetime = lifetime | ||||||
|  |         self.key_usage = set(key_usage.split(" ")) if key_usage else set() | ||||||
|  |         self.extended_key_usage = set(extended_key_usage.split(" ")) if extended_key_usage else set() | ||||||
|  |         if common_name.startswith("^"): | ||||||
|  |             self.common_name = common_name | ||||||
|  |         elif common_name == "RE_HOSTNAME": | ||||||
|  |             self.common_name = const.RE_HOSTNAME | ||||||
|  |         elif common_name == "RE_FQDN": | ||||||
|  |             self.common_name = const.RE_FQDN | ||||||
|  |         elif common_name == "RE_COMMON_NAME": | ||||||
|  |             self.common_name = const.RE_COMMON_NAME | ||||||
|  |         else: | ||||||
|  |             raise ValueError("Invalid common name constraint %s" % common_name) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def from_cert(self, cert): | ||||||
|  |         """ | ||||||
|  |         Derive signature profile from an already signed certificate, eg for renewal | ||||||
|  |         """ | ||||||
|  |         lifetime = cert["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None) - \ | ||||||
|  |             cert["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None) | ||||||
|  |         return SignatureProfile( | ||||||
|  |             None, "Renewal", cert.subject.native.get("organizational_unit_name"), | ||||||
|  |             cert.ca, lifetime.days, | ||||||
|  |             " ".join(cert.key_usage_value.native), | ||||||
|  |             " ".join(cert.extended_key_usage_value.native), "^") | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     def serialize(self): | ||||||
|  |         return dict([(key, getattr(self,key)) for key in ( | ||||||
|  |             "slug", "title", "ou", "ca", "lifetime", "key_usage", "extended_key_usage", "common_name")]) | ||||||
|  |  | ||||||
|  |     def __repr__(self): | ||||||
|  |         bits = [] | ||||||
|  |         if self.lifetime >= 365: | ||||||
|  |             bits.append("%d years" % (self.lifetime / 365)) | ||||||
|  |         if self.lifetime % 365: | ||||||
|  |             bits.append("%d days" % (self.lifetime % 365)) | ||||||
|  |         return "%s (title=%s, ca=%s, ou=%s, lifetime=%s, key_usage=%s, extended_key_usage=%s, common_name=%s)" % ( | ||||||
|  |             self.slug, self.title, self.ca, self.ou, " ".join(bits), self.key_usage, self.extended_key_usage, self.common_name) | ||||||
|  |  | ||||||
| @@ -30,11 +30,10 @@ | |||||||
|       </button> |       </button> | ||||||
|       <div class="dropdown-menu"> |       <div class="dropdown-menu"> | ||||||
|         {% for p in session.authority.signature.profiles %} |         {% for p in session.authority.signature.profiles %} | ||||||
|           <a class="dropdown-item{% if p.server and not request.server %} disabled{% endif %}" |           <a class="dropdown-item{% if not request.common_name.match(p.common_name) %} disabled{% endif %}" | ||||||
|             {% if p.server and not request.server %}title="Resubmit with FQDN as common name"{% endif %} |             {% if not request.common_name.match(p.common_name) %} title="Common name doesn't match expression {{ p.common_name }}"{% endif %} | ||||||
|             href="#" onclick="javascript:$.ajax({url:'/api/request/{{request.common_name}}/?sha256sum={{ request.sha256sum }}&profile={{ p.name }}',type:'post'});"> |             href="#" onclick="javascript:$.ajax({url:'/api/request/{{request.common_name}}/?sha256sum={{ request.sha256sum }}&profile={{ p.slug }}',type:'post'});"> | ||||||
|             {% if p.title %}{{ p.title }} ({% if p.server %}server{% else %}client{% endif %}){% else %} |             {{ p.title }}, expires in {{ p.lifetime }} days</a> | ||||||
|             {% if p.server %}Server{% else %}Client{% endif %}{% endif %}, expires in {{ p.lifetime }} days</a> |  | ||||||
|         {% endfor %} |         {% endfor %} | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   | |||||||
							
								
								
									
										52
									
								
								certidude/templates/server/profile.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								certidude/templates/server/profile.conf
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | |||||||
|  | [DEFAULT] | ||||||
|  | ou = | ||||||
|  | lifetime = 120 | ||||||
|  | ca = false | ||||||
|  | common name = RE_COMMON_NAME | ||||||
|  | key usage = digital_signature key_encipherment | ||||||
|  | extended key usage = | ||||||
|  |  | ||||||
|  | [ca] | ||||||
|  | title = Certificate Authority | ||||||
|  | common name = ^ca | ||||||
|  | ca = true | ||||||
|  | key usage = key_cert_sign crl_sign | ||||||
|  | extended key usage = | ||||||
|  | lifetime = 1095 | ||||||
|  |  | ||||||
|  | [rw] | ||||||
|  | title = Roadwarrior | ||||||
|  | ou = Roadwarrior | ||||||
|  | common name = RE_HOSTNAME | ||||||
|  | extended key usage = client_auth | ||||||
|  |  | ||||||
|  | [srv] | ||||||
|  | title = Server | ||||||
|  | ;ou = Server | ||||||
|  | common name = RE_FQDN | ||||||
|  | lifetime = 365 | ||||||
|  | extended key usage = server_auth | ||||||
|  |  | ||||||
|  | [gw] | ||||||
|  | title = Gateway | ||||||
|  | ou = Gateway | ||||||
|  | common name = RE_FQDN | ||||||
|  | renewable = true | ||||||
|  | lifetime = 30 | ||||||
|  | extended key usage = server_auth 1.3.6.1.5.5.8.2.2 client_auth | ||||||
|  |  | ||||||
|  | [ap] | ||||||
|  | title = Access Point | ||||||
|  | ou = Access Point | ||||||
|  | common name = RE_HOSTNAME | ||||||
|  | lifetime = 1825 | ||||||
|  | extended key usage = client_auth | ||||||
|  |  | ||||||
|  | [mfp] | ||||||
|  | title = Printers | ||||||
|  | ou = Printers | ||||||
|  | common name = ^mfp\- | ||||||
|  | lifetime = 30 | ||||||
|  | extended key usage = client_auth | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -194,14 +194,6 @@ lifetime = 30 | |||||||
| # Secret for generating and validating tokens, regenerate occasionally | # Secret for generating and validating tokens, regenerate occasionally | ||||||
| secret = {{ token_secret }} | secret = {{ token_secret }} | ||||||
|  |  | ||||||
| [profile] |  | ||||||
| # name, flags, lifetime, organizational unit, title |  | ||||||
| default = client, 120, Roadwarrior, Roadwarrior |  | ||||||
| gw = server, 30, Gateway, Gateway |  | ||||||
| srv = server, 365, Server, |  | ||||||
| ap = client, 1825, Access Point, Access Point |  | ||||||
| mfp = client, 30, MFP, Printers |  | ||||||
|  |  | ||||||
| [script] | [script] | ||||||
| # Path to the folder with scripts that can be served to the clients, set none to disable scripting | # Path to the folder with scripts that can be served to the clients, set none to disable scripting | ||||||
| path = {{ script_dir }} | path = {{ script_dir }} | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user