diff --git a/certidude/api/__init__.py b/certidude/api/__init__.py index 3d0c271..7facd0c 100644 --- a/certidude/api/__init__.py +++ b/certidude/api/__init__.py @@ -59,7 +59,7 @@ class SessionResource(object): user_mutliple_certificates=config.USER_MULTIPLE_CERTIFICATES, outbox = config.OUTBOX, certificate = authority.certificate, - events = config.PUSH_EVENT_SOURCE % config.PUSH_TOKEN, + events = config.EVENT_SOURCE_SUBSCRIBE % config.EVENT_SOURCE_TOKEN, requests=authority.list_requests(), signed=authority.list_signed(), revoked=authority.list_revoked(), diff --git a/certidude/api/request.py b/certidude/api/request.py index 9918851..00568e6 100644 --- a/certidude/api/request.py +++ b/certidude/api/request.py @@ -52,11 +52,11 @@ class RequestListResource(object): raise falcon.HTTPBadRequest( "Bad request", "Common name %s differs from Kerberos credential %s!" % (csr.common_name, machine)) - if csr.signable: - # Automatic enroll with Kerberos machine cerdentials - resp.set_header("Content-Type", "application/x-x509-user-cert") - resp.body = authority.sign(csr, overwrite=True).dump() - return + + # Automatic enroll with Kerberos machine cerdentials + resp.set_header("Content-Type", "application/x-x509-user-cert") + resp.body = authority.sign(csr, overwrite=True).dump() + return # Check if this request has been already signed and return corresponding certificte if it has been signed @@ -73,7 +73,7 @@ class RequestListResource(object): # TODO: check for revoked certificates and return HTTP 410 Gone # Process automatic signing if the IP address is whitelisted, autosigning was requested and certificate can be automatically signed - if req.get_param_as_bool("autosign") and csr.signable: + if req.get_param_as_bool("autosign") and csr.is_client: for subnet in config.AUTOSIGN_SUBNETS: if req.context.get("remote_addr") in subnet: try: @@ -103,7 +103,7 @@ class RequestListResource(object): # Wait the certificate to be signed if waiting is requested if req.get_param("wait"): # Redirect to nginx pub/sub - url = config.PUSH_LONG_POLL % csr.fingerprint() + url = config.LONG_POLL_SUBSCRIBE % csr.fingerprint() click.echo("Redirecting to: %s" % url) resp.status = falcon.HTTP_SEE_OTHER resp.set_header("Location", url.encode("ascii")) diff --git a/certidude/api/revoked.py b/certidude/api/revoked.py index 6fcc64b..9954e8e 100644 --- a/certidude/api/revoked.py +++ b/certidude/api/revoked.py @@ -27,7 +27,7 @@ class RevocationListResource(object): default_backend()).public_bytes(Encoding.DER) elif req.client_accepts("application/x-pem-file"): if req.get_param_as_bool("wait"): - url = config.PUSH_LONG_POLL % "crl" + url = config.LONG_POLL_SUBSCRIBE % "crl" resp.status = falcon.HTTP_SEE_OTHER resp.set_header("Location", url.encode("ascii")) logger.debug(u"Redirecting to CRL request to %s", url) diff --git a/certidude/authority.py b/certidude/authority.py index fb2ccaa..2ae8001 100644 --- a/certidude/authority.py +++ b/certidude/authority.py @@ -31,12 +31,7 @@ def publish_certificate(func): cert = func(csr, *args, **kwargs) assert isinstance(cert, Certificate), "notify wrapped function %s returned %s" % (func, type(cert)) - if cert.given_name and cert.surname and cert.email_address: - recipient = "%s %s <%s>" % (cert.given_name, cert.surname, cert.email_address) - elif cert.email_address: - recipient = cert.email_address - else: - recipient = None + recipient = None mailer.send( "certificate-signed.md", @@ -44,8 +39,8 @@ def publish_certificate(func): attachments=(cert,), certificate=cert) - if config.PUSH_PUBLISH: - url = config.PUSH_PUBLISH % csr.fingerprint() + if config.LONG_POLL_PUBLISH: + url = config.LONG_POLL_PUBLISH % csr.fingerprint() click.echo("Publishing certificate at %s ..." % url) requests.post(url, data=cert.dump(), headers={"User-Agent": "Certidude API", "Content-Type": "application/x-x509-user-cert"}) @@ -133,8 +128,8 @@ def revoke_certificate(common_name): push.publish("certificate-revoked", cert.common_name) # Publish CRL for long polls - if config.PUSH_PUBLISH: - url = config.PUSH_PUBLISH % "crl" + if config.LONG_POLL_PUBLISH: + url = config.LONG_POLL_PUBLISH % "crl" click.echo("Publishing CRL at %s ..." % url) requests.post(url, data=export_crl(), headers={"User-Agent": "Certidude API", "Content-Type": "application/x-pem-file"}) @@ -190,7 +185,7 @@ def delete_request(common_name): push.publish("request-deleted", request.common_name) # Write empty certificate to long-polling URL - requests.delete(config.PUSH_PUBLISH % request.fingerprint(), + requests.delete(config.LONG_POLL_PUBLISH % request.fingerprint(), headers={"User-Agent": "Certidude API"}) def generate_ovpn_bundle(common_name, owner=None): @@ -206,8 +201,6 @@ def generate_ovpn_bundle(common_name, owner=None): csr = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([ x509.NameAttribute(k, v) for k, v in ( (NameOID.COMMON_NAME, common_name), - (NameOID.GIVEN_NAME, owner and owner.given_name), - (NameOID.SURNAME, owner and owner.surname), ) if v ])) @@ -244,8 +237,6 @@ def generate_pkcs12_bundle(common_name, key_size=4096, owner=None): csr = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([ x509.NameAttribute(k, v) for k, v in ( (NameOID.COMMON_NAME, common_name), - (NameOID.GIVEN_NAME, owner and owner.given_name), - (NameOID.SURNAME, owner and owner.surname), ) if v ])) @@ -262,21 +253,26 @@ def generate_pkcs12_bundle(common_name, key_size=4096, owner=None): csr.sign(key, hashes.SHA512(), default_backend()).public_bytes(serialization.Encoding.PEM)), overwrite=True) # Generate P12, currently supported only by PyOpenSSL - from OpenSSL import crypto - p12 = crypto.PKCS12() - p12.set_privatekey( - crypto.load_privatekey( - crypto.FILETYPE_PEM, - key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption() + try: + from OpenSSL import crypto + except ImportError: + logger.error("For P12 bundles please install pyOpenSSL: pip install pyOpenSSL") + raise + else: + p12 = crypto.PKCS12() + p12.set_privatekey( + crypto.load_privatekey( + crypto.FILETYPE_PEM, + key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption() + ) ) ) - ) - p12.set_certificate( cert._obj ) - p12.set_ca_certificates([certificate._obj]) - return p12.export(), cert + p12.set_certificate( cert._obj ) + p12.set_ca_certificates([certificate._obj]) + return p12.export(), cert @publish_certificate diff --git a/certidude/cli.py b/certidude/cli.py index 94f63f1..f1e02da 100755 --- a/certidude/cli.py +++ b/certidude/cli.py @@ -305,8 +305,6 @@ def certidude_request(fork): @click.command("client", help="Setup X.509 certificates for application") @click.argument("server") @click.option("--common-name", "-cn", default=const.HOSTNAME, help="Common name, '%s' by default" % const.HOSTNAME) -@click.option("--org-unit", "-ou", help="Organizational unit") -@click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, '%s' by default" % EMAIL) @click.option("--given-name", "-gn", default=FIRST_NAME, help="Given name of the person associted with the certificate, '%s' by default" % FIRST_NAME) @click.option("--surname", "-sn", default=SURNAME, help="Surname of the person associted with the certificate, '%s' by default" % SURNAME) @click.option("--key-usage", "-ku", help="Key usage attributes, none requested by default") @@ -325,8 +323,6 @@ def certidude_setup_client(quiet, **kwargs): @click.command("server", help="Set up OpenVPN server") @click.argument("authority") -@click.option("--org-unit", "-ou", help="Organizational unit") -@click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, '%s' by default" % EMAIL) @click.option("--subnet", "-s", default="192.168.33.0/24", type=ip_network, help="OpenVPN subnet, 192.168.33.0/24 by default") @click.option("--local", "-l", default="0.0.0.0", help="OpenVPN listening address, defaults to all interfaces") @click.option("--port", "-p", default=1194, type=click.IntRange(1,60000), help="OpenVPN listening port, 1194 by default") @@ -336,7 +332,7 @@ def certidude_setup_client(quiet, **kwargs): default="/etc/openvpn/site-to-client.conf", type=click.File(mode="w", atomic=True, lazy=True), help="OpenVPN configuration file") -def certidude_setup_openvpn_server(authority, config, subnet, route, email_address, org_unit, local, proto, port): +def certidude_setup_openvpn_server(authority, config, subnet, route, org_unit, local, proto, port): # TODO: Make dirs # TODO: Intelligent way of getting last IP address in the subnet @@ -428,7 +424,6 @@ def certidude_setup_openvpn_server(authority, config, subnet, route, email_addre @click.command("nginx", help="Set up nginx as HTTPS server") @click.argument("server") @click.option("--common-name", "-cn", default=const.FQDN, help="Common name, %s by default" % const.FQDN) -@click.option("--org-unit", "-ou", help="Organizational unit") @click.option("--tls-config", default="/etc/nginx/conf.d/tls.conf", type=click.File(mode="w", atomic=True, lazy=True), @@ -496,7 +491,6 @@ def certidude_setup_nginx(authority, site_config, tls_config, common_name, org_u @click.argument("authority") @click.argument("remote") @click.option('--proto', "-t", default="udp", type=click.Choice(['udp', 'tcp']), help="OpenVPN transport protocol, UDP by default") -@click.option("--org-unit", "-ou", help="Organizational unit") @click.option("--config", "-o", default="/etc/openvpn/client-to-site.conf", type=click.File(mode="w", atomic=True, lazy=True), @@ -568,12 +562,10 @@ def certidude_setup_openvpn_client(authority, remote, config, org_unit, proto): @click.command("server", help="Set up strongSwan server") @click.argument("server") -@click.option("--org-unit", "-ou", help="Organizational unit") -@click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, %s by default" % EMAIL) @click.option("--subnet", "-sn", default=u"192.168.33.0/24", type=ip_network, help="IPsec virtual subnet, 192.168.33.0/24 by default") @click.option("--local", "-l", type=ip_address, help="IP address associated with the certificate, none by default") @click.option("--route", "-r", type=ip_network, multiple=True, help="Subnets to advertise via this connection, multiple allowed") -def certidude_setup_strongswan_server(authority, config, secrets, subnet, route, email_address, org_unit, local, fqdn): +def certidude_setup_strongswan_server(authority, config, secrets, subnet, route, local, fqdn): if "." not in common_name: raise ValueError("Hostname has to be fully qualified!") if not local: @@ -627,7 +619,6 @@ def certidude_setup_strongswan_server(authority, config, secrets, subnet, route, @click.command("client", help="Set up strongSwan client") @click.argument("server") @click.argument("remote") -@click.option("--org-unit", "-ou", help="Organizational unit") def certidude_setup_strongswan_client(authority, config, org_unit, remote, dpdaction): # Create corresponding section in /etc/certidude/client.conf client_config = ConfigParser() @@ -675,7 +666,6 @@ def certidude_setup_strongswan_client(authority, config, org_unit, remote, dpdac @click.command("networkmanager", help="Set up strongSwan client via NetworkManager") @click.argument("server") # Certidude server @click.argument("remote") # StrongSwan gateway -@click.option("--org-unit", "-ou", help="Organizational unit") def certidude_setup_strongswan_networkmanager(server,remote, org_unit): endpoint = "IPSec to %s" % remote @@ -721,9 +711,7 @@ def certidude_setup_strongswan_networkmanager(server,remote, org_unit): @click.argument("server") # Certidude server @click.argument("remote") # OpenVPN gateway @click.option("--common-name", "-cn", default=const.HOSTNAME, help="Common name, %s by default" % const.HOSTNAME) -@click.option("--org-unit", "-ou", help="Organizational unit") -@click.option("--email-address", "-m", help="E-mail associated with the request, none by default") -def certidude_setup_openvpn_networkmanager(authority, email_address, org_unit, remote): +def certidude_setup_openvpn_networkmanager(authority, org_unit, remote): # Create corresponding section in /etc/certidude/client.conf client_config = ConfigParser() if os.path.exists(const.CLIENT_CONFIG_PATH): @@ -781,11 +769,10 @@ def certidude_setup_openvpn_networkmanager(authority, email_address, org_unit, r @click.option("--revoked-url", default=None, help="CRL distribution URL") @click.option("--certificate-url", default=None, help="Authority certificate URL") @click.option("--push-server", default="http://" + const.FQDN, help="Push server, by default http://%s" % const.FQDN) -@click.option("--email-address", default="certidude@" + const.FQDN, help="E-mail address of the CA") @click.option("--directory", help="Directory for authority files") @click.option("--server-flags", is_flag=True, help="Add TLS Server and IKE Intermediate extended key usage flags") @click.option("--outbox", default="smtp://smtp.%s" % const.DOMAIN, help="SMTP server, smtp://smtp.%s by default" % const.DOMAIN) -def certidude_setup_authority(username, static_path, kerberos_keytab, nginx_config, parent, country, state, locality, organization, organizational_unit, common_name, directory, certificate_lifetime, authority_lifetime, revocation_list_lifetime, revoked_url, certificate_url, push_server, email_address, outbox, server_flags): +def certidude_setup_authority(username, static_path, kerberos_keytab, nginx_config, parent, country, state, locality, organization, organizational_unit, common_name, directory, certificate_lifetime, authority_lifetime, revocation_list_lifetime, revoked_url, certificate_url, push_server, outbox, server_flags): if not directory: if os.getuid(): @@ -833,10 +820,11 @@ def certidude_setup_authority(username, static_path, kerberos_keytab, nginx_conf name = cp.get("global", "netbios name") base = ",".join(["dc=" + j for j in domain.split(".")]) - with open("/etc/cron.hourly/certidude", "w") as fh: - fh.write(env.get_template("ldap-ticket-renewal.sh").render(vars())) - os.chmod("/etc/cron.hourly/certidude", 0o755) - click.echo("Created /etc/cron.hourly/certidude for automatic LDAP service ticket renewal, inspect and adjust accordingly") + if not os.path.exists("/etc/cron.hourly/certidude"): + with open("/etc/cron.hourly/certidude", "w") as fh: + fh.write(env.get_template("ldap-ticket-renewal.sh").render(vars())) + os.chmod("/etc/cron.hourly/certidude", 0o755) + click.echo("Created /etc/cron.hourly/certidude for automatic LDAP service ticket renewal, inspect and adjust accordingly") os.system("/etc/cron.hourly/certidude") else: click.echo("Warning: /etc/krb5.keytab or /etc/samba/smb.conf not found, Kerberos unconfigured") @@ -919,10 +907,10 @@ def certidude_setup_authority(username, static_path, kerberos_keytab, nginx_conf ).not_valid_after( datetime.utcnow() + timedelta(days=authority_lifetime) ).serial_number(1 - ).add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True, + ).add_extension(x509.BasicConstraints(ca=True, path_length=0), critical=True, ).add_extension(x509.KeyUsage( - digital_signature=True, - key_encipherment=False, + digital_signature=server_flags, + key_encipherment=server_flags, content_commitment=False, data_encipherment=False, key_agreement=False, @@ -930,12 +918,6 @@ def certidude_setup_authority(username, static_path, kerberos_keytab, nginx_conf crl_sign=True, encipher_only=False, decipher_only=False), critical=True, - ).add_extension( - x509.SubjectAlternativeName([ - x509.DNSName(common_name), - x509.RFC822Name(email_address) - ]), - critical=False, ).add_extension( x509.SubjectKeyIdentifier.from_public_key(key.public_key()), critical=False @@ -1132,34 +1114,7 @@ def certidude_list(verbose, show_key_type, show_extensions, show_path, show_sign def certidude_sign(common_name, overwrite, lifetime): from certidude import authority, config request = authority.get_request(common_name) - - # Use signer if this is regular client CSR - if request.signable: - # Sign via signer process - cert = authority.sign(request) - - # Sign directly if it's eg. TLS server CSR - else: - # Load CA private key and certificate - private_key = serialization.load_pem_private_key( - open(config.AUTHORITY_PRIVATE_KEY_PATH).read(), - password=None, # TODO: Ask password for private key? - backend=default_backend()) - authority_certificate = x509.load_pem_x509_certificate( - open(config.AUTHORITY_CERTIFICATE_PATH).read(), - backend=default_backend()) - - # Drop privileges - # to use LDAP service ticket to read usernames of the admins group - # in order to send e-mail - _, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude") - os.setgroups([]) - os.setgid(gid) - os.setuid(uid) - - # Sign directly using private key - cert = authority.sign2(request, private_key, authority_certificate, - overwrite, True, lifetime) + cert = authority.sign(request) @click.command("serve", help="Run server") @@ -1292,9 +1247,9 @@ def certidude_serve(port, listen): elif config.LOGGING_BACKEND: raise ValueError("Invalid logging.backend = %s" % config.LOGGING_BACKEND) - if config.PUSH_PUBLISH: - from certidude.push import PushLogHandler - log_handlers.append(PushLogHandler()) + if config.EVENT_SOURCE_PUBLISH: + from certidude.push import EventSourceLogHandler + log_handlers.append(EventSourceLogHandler()) for facility in "api", "cli": logger = logging.getLogger(facility) @@ -1310,7 +1265,6 @@ def certidude_serve(port, listen): atexit.register(exit_handler) logging.getLogger("cli").debug("Started Certidude at %s", const.FQDN) - print "Ready" httpd.serve_forever() @click.group("strongswan", help="strongSwan helpers") diff --git a/certidude/config.py b/certidude/config.py index 401aa0c..5caaab1 100644 --- a/certidude/config.py +++ b/certidude/config.py @@ -38,7 +38,10 @@ AUTHORITY_CERTIFICATE_PATH = cp.get("authority", "certificate path") REQUESTS_DIR = cp.get("authority", "requests dir") SIGNED_DIR = cp.get("authority", "signed dir") REVOKED_DIR = cp.get("authority", "revoked dir") -OUTBOX = cp.get("authority", "outbox") + +OUTBOX = cp.get("authority", "outbox uri") +OUTBOX_NAME = cp.get("authority", "outbox sender name") +OUTBOX_MAIL = cp.get("authority", "outbox sender address") BUNDLE_FORMAT = cp.get("authority", "bundle format") OPENVPN_BUNDLE_TEMPLATE = cp.get("authority", "openvpn bundle template") @@ -59,10 +62,11 @@ CERTIFICATE_CRL_URL = cp.get("signature", "revoked url") REVOCATION_LIST_LIFETIME = cp.getint("signature", "revocation list lifetime") -PUSH_TOKEN = cp.get("push", "token") -PUSH_EVENT_SOURCE = cp.get("push", "event source") -PUSH_LONG_POLL = cp.get("push", "long poll") -PUSH_PUBLISH = cp.get("push", "publish") +EVENT_SOURCE_TOKEN = cp.get("push", "event source token") +EVENT_SOURCE_PUBLISH = cp.get("push", "event source publish") +EVENT_SOURCE_SUBSCRIBE = cp.get("push", "event source subscribe") +LONG_POLL_PUBLISH = cp.get("push", "long poll publish") +LONG_POLL_SUBSCRIBE = cp.get("push", "long poll subscribe") TAGGING_BACKEND = cp.get("tagging", "backend") LOGGING_BACKEND = cp.get("logging", "backend") diff --git a/certidude/decorators.py b/certidude/decorators.py index 20b24cf..40c0ee0 100644 --- a/certidude/decorators.py +++ b/certidude/decorators.py @@ -52,12 +52,12 @@ def event_source(func): return wrapped class MyEncoder(json.JSONEncoder): - REQUEST_ATTRIBUTES = "signable", "identity", "changed", "common_name", \ - "organizational_unit", "given_name", "surname", "fqdn", "email_address", \ + REQUEST_ATTRIBUTES = "is_client", "identity", "changed", "common_name", \ + "organizational_unit", "fqdn", \ "key_type", "key_length", "md5sum", "sha1sum", "sha256sum", "key_usage" CERTIFICATE_ATTRIBUTES = "revokable", "identity", "common_name", \ - "organizational_unit", "given_name", "surname", "fqdn", "email_address", \ + "organizational_unit", "fqdn", \ "key_type", "key_length", "sha256sum", "serial_number", "key_usage", \ "signed", "expires" diff --git a/certidude/helpers.py b/certidude/helpers.py index f023376..b396613 100644 --- a/certidude/helpers.py +++ b/certidude/helpers.py @@ -15,7 +15,7 @@ from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID, AuthorityInforma from configparser import ConfigParser from OpenSSL import crypto -def certidude_request_certificate(server, key_path, request_path, certificate_path, authority_path, revocations_path, common_name, extended_key_usage_flags=None, org_unit=None, email_address=None, given_name=None, surname=None, autosign=False, wait=False, ip_address=None, dns=None, bundle=False, insecure=False): +def certidude_request_certificate(server, key_path, request_path, certificate_path, authority_path, revocations_path, common_name, autosign=False, wait=False, ip_address=None, bundle=False, insecure=False): """ Exchange CSR for certificate using Certidude HTTP API server """ @@ -127,40 +127,11 @@ def certidude_request_certificate(server, key_path, request_path, certificate_pa # Set subject name attributes names = [x509.NameAttribute(NameOID.COMMON_NAME, common_name.decode("utf-8"))] - if given_name: - names.append(x509.NameAttribute(NameOID.GIVEN_NAME, given_name.decode("utf-8"))) - if surname: - names.append(x509.NameAttribute(NameOID.SURNAME, surname.decode("utf-8"))) - if org_unit: - names.append(x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT, org_unit.decode("utf-8"))) - - # Collect subject alternative names - subject_alt_names = set() - if email_address: - subject_alt_names.add(x509.RFC822Name(email_address)) - if ip_address: - subject_alt_names.add("IP:%s" % ip_address) - if dns: - subject_alt_names.add(x509.DNSName(dns)) - # Construct CSR csr = x509.CertificateSigningRequestBuilder( ).subject_name(x509.Name(names)) - - if extended_key_usage_flags: - click.echo("Adding extended key usage extension: %s" % extended_key_usage_flags) - csr = csr.add_extension(x509.ExtendedKeyUsage( - extended_key_usage_flags), critical=True) - - if subject_alt_names: - click.echo("Adding subject alternative name extension: %s" % subject_alt_names) - csr = csr.add_extension( - x509.SubjectAlternativeName(subject_alt_names), - critical=False) - - # Sign & dump CSR os.umask(0o022) with open(request_path + ".part", "wb") as f: diff --git a/certidude/mailer.py b/certidude/mailer.py index 6efcba7..f5365c0 100644 --- a/certidude/mailer.py +++ b/certidude/mailer.py @@ -70,7 +70,7 @@ def send(template, to=None, attachments=(), **context): msg = MIMEMultipart("alternative") msg["Subject"] = subject - msg["From"] = authority.certificate.email_address + msg["From"] = "%s <%s>" % (config.OUTBOX_NAME, config.OUTBOX_MAIL) msg["To"] = recipients part1 = MIMEText(text, "plain") @@ -93,4 +93,4 @@ def send(template, to=None, attachments=(), **context): if username and password: conn.login(username, password) - conn.sendmail(authority.certificate.email_address, recipients, msg.as_string()) + conn.sendmail(config.OUTBOX_MAIL, recipients, msg.as_string()) diff --git a/certidude/push.py b/certidude/push.py index 535bdf7..08ce8ed 100644 --- a/certidude/push.py +++ b/certidude/push.py @@ -9,9 +9,9 @@ from certidude import config def publish(event_type, event_data): """ - Publish event on push server + Publish event on nchan EventSource publisher """ - if not config.PUSH_PUBLISH: + if not config.EVENT_SOURCE_PUBLISH: # Push server disabled return @@ -19,7 +19,7 @@ def publish(event_type, event_data): from certidude.decorators import MyEncoder event_data = json.dumps(event_data, cls=MyEncoder) - url = config.PUSH_PUBLISH % config.PUSH_TOKEN + url = config.EVENT_SOURCE_PUBLISH % config.EVENT_SOURCE_TOKEN click.echo("Publishing %s event '%s' on %s" % (event_type, event_data, url)) try: @@ -38,12 +38,11 @@ def publish(event_type, event_data): click.echo("Failed to submit event to push server, connection error") -class PushLogHandler(logging.Handler): +class EventSourceLogHandler(logging.Handler): """ To be used with Python log handling framework for publishing log entries """ def emit(self, record): - from certidude.push import publish publish("log-entry", dict( created = datetime.utcfromtimestamp(record.created), message = record.msg % record.args, diff --git a/certidude/signer.py b/certidude/signer.py index 8927dfa..71d03dc 100644 --- a/certidude/signer.py +++ b/certidude/signer.py @@ -61,34 +61,67 @@ class SignHandler(asynchat.async_chat): NotImplemented # TODO: Implement OCSP elif cmd == "sign-request": + # Only common name and public key are used from request request = x509.load_pem_x509_csr(body, default_backend()) - subject = x509.Name([n for n in request.subject if n.oid in DN_WHITELIST]) + common_name, = request.subject.get_attributes_for_oid(NameOID.COMMON_NAME) + + + #subject = x509.Name([n for n in request.subject if n.oid in DN_WHITELIST]) + + # If common name is a fully qualified name assume it has to be signed + # with server certificate flags + server_flags = "." in common_name.value + + # TODO: For fqdn allow autosign with validation + + extended_key_usage_flags = [] + if server_flags: + extended_key_usage_flags.append( # IKE intermediate for IPSec + x509.ObjectIdentifier("1.3.6.1.5.5.8.2.2")) + extended_key_usage_flags.append( # OpenVPN server + ExtendedKeyUsageOID.SERVER_AUTH) + else: + extended_key_usage_flags.append( # OpenVPN client + ExtendedKeyUsageOID.CLIENT_AUTH) cert = x509.CertificateBuilder( - ).subject_name(subject + ).subject_name( + x509.Name([common_name]) ).serial_number(random.randint( 0x1000000000000000000000000000000000000000, 0xffffffffffffffffffffffffffffffffffffffff) - ).issuer_name(self.server.certificate.issuer - ).public_key(request.public_key() - ).not_valid_before(now - timedelta(hours=1) - ).not_valid_after(now + timedelta(days=config.CERTIFICATE_LIFETIME) - ).add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True, - ).add_extension(x509.KeyUsage( - digital_signature=True, - key_encipherment=True, - content_commitment=False, - data_encipherment=False, - key_agreement=False, - key_cert_sign=False, - crl_sign=False, - encipher_only=False, - decipher_only=False), critical=True, - ).add_extension(x509.ExtendedKeyUsage( - [ExtendedKeyUsageOID.CLIENT_AUTH] - ), critical=True, + ).issuer_name( + self.server.certificate.issuer + ).public_key( + request.public_key() + ).not_valid_before( + now - timedelta(hours=1) + ).not_valid_after( + now + timedelta(days=config.CERTIFICATE_LIFETIME) ).add_extension( - x509.SubjectKeyIdentifier.from_public_key(request.public_key()), + x509.BasicConstraints( + ca=False, + path_length=None), + critical=True, + ).add_extension( + x509.KeyUsage( + digital_signature=True, + key_encipherment=True, + content_commitment=False, + data_encipherment=False, + key_agreement=False, + key_cert_sign=False, + crl_sign=False, + encipher_only=False, + decipher_only=False), + critical=True, + ).add_extension( + x509.ExtendedKeyUsage( + extended_key_usage_flags), + critical=True, + ).add_extension( + x509.SubjectKeyIdentifier.from_public_key( + request.public_key()), critical=False ).add_extension( x509.AuthorityInformationAccess([ diff --git a/certidude/static/views/request.html b/certidude/static/views/request.html index 651f458..f9b815e 100644 --- a/certidude/static/views/request.html +++ b/certidude/static/views/request.html @@ -1,11 +1,7 @@
  • Fetch -{% if request.signable %} -{% else %} - -{% endif %} diff --git a/certidude/templates/certidude-server.conf b/certidude/templates/certidude-server.conf index 4fe3c23..abcecc6 100644 --- a/certidude/templates/certidude-server.conf +++ b/certidude/templates/certidude-server.conf @@ -52,19 +52,23 @@ request subnets = 0.0.0.0/0 autosign subnets = 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 [logging] -backend = sql +backend = + +;backend = sql database = sqlite://{{ directory }}/db.sqlite [tagging] -backend = sql +backend = + +;backend = sql database = sqlite://{{ directory }}/db.sqlite [leases] backend = ;backend = sql -;schema = strongswan -;database = sqlite://{{ directory }}/db.sqlite +schema = strongswan +database = sqlite://{{ directory }}/db.sqlite # Following was used on an OpenWrt router # uci set openvpn.s2c.status=/www/status.log @@ -80,10 +84,11 @@ certificate url = {{ certificate_url }} revoked url = {{ revoked_url }} [push] -token = {{ push_token }} -event source = {{ push_server }}/ev/%s -long poll = {{ push_server }}/lp/%s -publish = {{ push_server }}/pub?id=%s +event source token = {{ push_token }} +event source subscribe = {{ push_server }}/ev/sub/%s +event source publish = {{ push_server }}/ev/pub/%s +long poll subscribe = {{ push_server }}/lp/sub/%s +long poll publish = {{ push_server }}/lp/pub/%s [authority] # User certificate enrollment specifies whether logged in users are allowed to @@ -102,7 +107,10 @@ requests dir = {{ directory }}/requests/ signed dir = {{ directory }}/signed/ revoked dir = {{ directory }}/revoked/ expired dir = {{ directory }}/expired/ -outbox = {{ outbox }} + +outbox uri = {{ outbox }} +outbox sender name = Certificate management +outbox sender address = certificates@example.com bundle format = p12 ;bundle format = ovpn diff --git a/certidude/templates/nginx.conf b/certidude/templates/nginx.conf index 41efc19..bb44986 100644 --- a/certidude/templates/nginx.conf +++ b/certidude/templates/nginx.conf @@ -17,21 +17,28 @@ server { } {% if not push_server %} - location /pub { + location ~ "^/lp/pub/(.*)" { allow 127.0.0.1; - nchan_publisher http; - nchan_store_messages off; - nchan_channel_id $arg_id; + nchan_publisher; + nchan_channel_id $1; + nchan_message_buffer_length 0; } - location ~ "^/lp/(.*)" { + location ~ "^/ev/pub/(.*)" { + allow 127.0.0.1; + nchan_publisher; + nchan_channel_id $1; + nchan_message_buffer_length 0; + } + + location ~ "^/lp/sub/(.*)" { + nchan_channel_id $1; nchan_subscriber longpoll; - nchan_channel_id $1; } - location ~ "^/ev/(.*)" { - nchan_subscriber eventsource; + location ~ "^/ev/sub/(.*)" { nchan_channel_id $1; + nchan_subscriber eventsource; } {% endif %} diff --git a/certidude/wrappers.py b/certidude/wrappers.py index c77d658..9996c61 100644 --- a/certidude/wrappers.py +++ b/certidude/wrappers.py @@ -22,22 +22,6 @@ class CertificateBase: def __repr__(self): return self.buf - @property - def given_name(self): - return self.subject.GN - - @given_name.setter - def given_name(self, value): - return setattr(self.subject, "GN", value) - - @property - def surname(self): - return self.subject.SN - - @surname.setter - def surname(self, value): - return setattr(self.subject, "SN", value) - @property def common_name(self): return self.subject.CN @@ -46,46 +30,6 @@ class CertificateBase: def common_name(self, value): self.subject.CN = value - @property - def country_code(self): - return getattr(self._obj.get_subject(), "C", None) - - @property - def state_or_county(self): - return getattr(self._obj.get_subject(), "S", None) - - @property - def city(self): - return getattr(self._obj.get_subject(), "L", None) - - @property - def organization(self): - return getattr(self._obj.get_subject(), "O", None) - - @property - def organizational_unit(self): - return getattr(self._obj.get_subject(), "OU", None) - - @country_code.setter - def country_code(self, value): - return setattr(self._obj.get_subject(), "C", value) - - @state_or_county.setter - def state_or_county(self, value): - return setattr(self._obj.get_subject(), "S", value) - - @city.setter - def city(self, value): - return setattr(self._obj.get_subject(), "L", value) - - @organization.setter - def organization(self, value): - return setattr(self._obj.get_subject(), "O", value) - - @organizational_unit.setter - def organizational_unit(self, value): - return setattr(self._obj.get_subject(), "OU", value) - @property def key_usage(self): def iterate(): @@ -139,13 +83,6 @@ class CertificateBase: critical, value.encode("ascii")) for (key,value,critical) in extensions]) - @property - def email_address(self): - for bit in self.subject_alt_name.split(", "): - if bit.startswith("email:"): - return bit[6:] - return "" - @property def fqdn(self): for bit in self.subject_alt_name.split(", "): @@ -153,17 +90,6 @@ class CertificateBase: return bit[4:] return "" - @property - def subject_alt_name(self): - for key, value, data in self.extensions: - if key == "subjectAltName": - return value - return "" - - @subject_alt_name.setter - def subject_alt_name(self, value): - self.set_extension("subjectAltName", value, False) - @property def pubkey(self): from Crypto.Util import asn1 @@ -226,11 +152,12 @@ class Request(CertificateBase): assert not self.buf or self.buf == self.dump(), "%s is not %s" % (repr(self.buf), repr(self.dump())) @property - def signable(self): - for key, value, data in self.extensions: - if key not in const.EXTENSION_WHITELIST: - return False - return True + def is_server(self): + return "." in self.common_name + + @property + def is_client(self): + return not self.is_server def dump(self): return crypto.dump_certificate_request(crypto.FILETYPE_PEM, self._obj).decode("ascii")