diff --git a/certidude/api/__init__.py b/certidude/api/__init__.py index c9244cb..982d67f 100644 --- a/certidude/api/__init__.py +++ b/certidude/api/__init__.py @@ -123,18 +123,19 @@ def certidude_app(): message = record.msg % record.args, severity = record.levelname.lower())) - sql_handler = MySQLLogHandler(config.DATABASE_POOL) + if config.DATABASE_POOL: + sql_handler = MySQLLogHandler(config.DATABASE_POOL) push_handler = PushLogHandler() for facility in "api", "cli": logger = logging.getLogger(facility) logger.setLevel(logging.DEBUG) - logger.addHandler(sql_handler) + if config.DATABASE_POOL: + logger.addHandler(sql_handler) logger.addHandler(push_handler) - logging.getLogger("cli").debug("Started Certidude at %s", - socket.getaddrinfo(socket.gethostname(), 0, flags=socket.AI_CANONNAME)[0][3]) + logging.getLogger("cli").debug("Started Certidude at %s", config.FQDN) import atexit diff --git a/certidude/api/request.py b/certidude/api/request.py index fe6fb0a..0b500d8 100644 --- a/certidude/api/request.py +++ b/certidude/api/request.py @@ -22,7 +22,7 @@ class RequestListResource(object): Submit certificate signing request (CSR) in PEM format """ # Parse remote IPv4/IPv6 address - remote_addr = ipaddress.ip_network(req.env["REMOTE_ADDR"]) + remote_addr = ipaddress.ip_network(req.env["REMOTE_ADDR"].decode("utf-8")) # Check for CSR submission whitelist if config.REQUEST_SUBNETS: @@ -30,7 +30,7 @@ class RequestListResource(object): if subnet.overlaps(remote_addr): break else: - logger.warning("Attempted to submit signing request from non-whitelisted address %s", req.env["REMOTE_ADDR"]) + logger.warning("Attempted to submit signing request from non-whitelisted address %s", remote_addr) raise falcon.HTTPForbidden("Forbidden", "IP address %s not whitelisted" % remote_addr) if req.get_header("Content-Type") != "application/pkcs10": diff --git a/certidude/auth.py b/certidude/auth.py index 6593d19..7ed116d 100644 --- a/certidude/auth.py +++ b/certidude/auth.py @@ -18,7 +18,7 @@ logger = logging.getLogger("api") # address eg via LDAP, hence to keep things simple # we simply use Kerberos to authenticate. -FQDN = socket.getaddrinfo(socket.gethostname(), 0, flags=socket.AI_CANONNAME)[0][3] +FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3] if not os.getenv("KRB5_KTNAME"): click.echo("Kerberos keytab not specified, set environment variable 'KRB5_KTNAME'", err=True) @@ -89,7 +89,7 @@ def authorize_admin(func): def wrapped(self, req, resp, *args, **kwargs): from certidude import config # Parse remote IPv4/IPv6 address - remote_addr = ipaddress.ip_network(req.env["REMOTE_ADDR"]) + remote_addr = ipaddress.ip_network(req.env["REMOTE_ADDR"].decode("utf-8")) # Check for administration subnet whitelist print("Comparing:", config.ADMIN_SUBNETS, "To:", remote_addr) @@ -97,12 +97,12 @@ def authorize_admin(func): if subnet.overlaps(remote_addr): break else: - logger.info("Rejected access to administrative call %s by %s from %s, source address not whitelisted", req.env["PATH_INFO"], req.context["user"], req.env["REMOTE_ADDR"]) + logger.info("Rejected access to administrative call %s by %s from %s, source address not whitelisted", req.env["PATH_INFO"], req.context["user"], remote_addr) raise falcon.HTTPForbidden("Forbidden", "Remote address %s not whitelisted" % remote_addr) # Check for username whitelist if req.context.get("user") not in config.ADMIN_USERS: - logger.info("Rejected access to administrative call %s by %s from %s, user not whitelisted", req.env["PATH_INFO"], req.context["user"], req.env["REMOTE_ADDR"]) + logger.info("Rejected access to administrative call %s by %s from %s, user not whitelisted", req.env["PATH_INFO"], req.context["user"], remote_addr) raise falcon.HTTPForbidden("Forbidden", "User %s not whitelisted" % req.context.get("user")) # Retain username, TODO: Better abstraction with username, e-mail, sn, gn? diff --git a/certidude/authority.py b/certidude/authority.py index da5c9fd..9e9e98d 100644 --- a/certidude/authority.py +++ b/certidude/authority.py @@ -3,7 +3,7 @@ import click import os import re import socket -import urllib.request +import requests from OpenSSL import crypto from certidude import config, push from certidude.wrappers import Certificate, Request @@ -24,12 +24,9 @@ def publish_certificate(func): if config.PUSH_PUBLISH: url = config.PUSH_PUBLISH % csr.fingerprint() - notification = urllib.request.Request(url, cert.dump().encode("ascii")) - notification.add_header("User-Agent", "Certidude API") - notification.add_header("Content-Type", "application/x-x509-user-cert") - click.echo("Publishing certificate at %s, waiting for response..." % url) - response = urllib.request.urlopen(notification) - response.read() + 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"}) push.publish("request-signed", csr.common_name) return cert return wrapped @@ -146,22 +143,8 @@ def delete_request(common_name): push.publish("request-deleted", request_sha1sum) # Write empty certificate to long-polling URL - url = config.PUSH_PUBLISH % request_sha1sum - click.echo("POST-ing empty certificate at %s, waiting for response..." % url) - publisher = urllib.request.Request(url, b"-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----\n") - publisher.add_header("User-Agent", "Certidude API") - - try: - response = urllib.request.urlopen(publisher) - body = response.read() - except urllib.error.HTTPError as err: - if err.code == 404: - print("No subscribers on the channel") - else: - raise - else: - print("Push server returned:", response.code, body) - + requests.delete(config.PUSH_PUBLISH % request_sha1sum, + headers={"User-Agent": "Certidude API"}) @publish_certificate def sign(req, overwrite=False, delete=True): diff --git a/certidude/cli.py b/certidude/cli.py index 7b55e11..5e478f9 100755 --- a/certidude/cli.py +++ b/certidude/cli.py @@ -23,7 +23,8 @@ from jinja2 import Environment, PackageLoader from time import sleep from setproctitle import setproctitle from OpenSSL import crypto - +from future.standard_library import install_aliases +install_aliases() env = Environment(loader=PackageLoader("certidude", "templates"), trim_blocks=True) @@ -44,7 +45,7 @@ assert hasattr(crypto.X509Req(), "get_extensions"), "You're running too old vers # Parse command-line argument defaults from environment HOSTNAME = socket.gethostname() -FQDN = socket.getaddrinfo(HOSTNAME, 0, flags=socket.AI_CANONNAME)[0][3] +FQDN = socket.getaddrinfo(HOSTNAME, 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3] USERNAME = os.environ.get("USER") NOW = datetime.utcnow().replace(tzinfo=None) FIRST_NAME = None @@ -66,9 +67,8 @@ if os.getuid() >= 1000: @click.option("-f", "--fork", default=False, is_flag=True, help="Fork to background") def certidude_request_spawn(fork): from certidude.helpers import certidude_request_certificate - from configparser import ConfigParser - clients = ConfigParser() + clients = configparser.ConfigParser() clients.readfp(open("/etc/certidude/client.conf")) services = ConfigParser() diff --git a/certidude/config.py b/certidude/config.py index 8ec6588..847ba95 100644 --- a/certidude/config.py +++ b/certidude/config.py @@ -1,14 +1,18 @@ import click +import codecs import configparser import ipaddress import os import socket import string from random import choice +from urllib.parse import urlparse + +FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3] cp = configparser.ConfigParser() -cp.read("/etc/certidude/server.conf") +cp.readfp(codecs.open("/etc/certidude/server.conf", "r", "utf8")) ADMIN_USERS = set([j for j in cp.get("authorization", "admin_users").split(" ") if j]) ADMIN_SUBNETS = set([ipaddress.ip_network(j) for j in cp.get("authorization", "admin_subnets").split(" ") if j]) @@ -43,14 +47,16 @@ try: PUSH_LONG_POLL = cp.get("push", "long_poll") PUSH_PUBLISH = cp.get("push", "publish") except configparser.NoOptionError: - PUSH_SERVER = cp.get("push", "server") + PUSH_SERVER = cp.get("push", "server") or "http://localhost" PUSH_EVENT_SOURCE = PUSH_SERVER + "/ev/%s" PUSH_LONG_POLL = PUSH_SERVER + "/lp/%s" PUSH_PUBLISH = PUSH_SERVER + "/pub?id=%s" -from urllib.parse import urlparse -o = urlparse(cp.get("authority", "database")) -if o.scheme == "mysql": +o = urlparse(cp.get("authority", "database") if cp.has_option("authority", "database") else "") + +if not o.scheme: + DATABASE_POOL = None +elif o.scheme == "mysql": import mysql.connector DATABASE_POOL = mysql.connector.pooling.MySQLConnectionPool( pool_size = 32, diff --git a/certidude/decorators.py b/certidude/decorators.py index 8ddce56..2771959 100644 --- a/certidude/decorators.py +++ b/certidude/decorators.py @@ -1,3 +1,4 @@ +# encoding: utf-8 import falcon import ipaddress @@ -41,8 +42,6 @@ class MyEncoder(json.JSONEncoder): return obj.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z" if isinstance(obj, date): return obj.strftime("%Y-%m-%d") - if isinstance(obj, map): - return tuple(obj) if isinstance(obj, types.GeneratorType): return tuple(obj) if isinstance(obj, Request): diff --git a/certidude/helpers.py b/certidude/helpers.py index 5a3ad01..af2a782 100644 --- a/certidude/helpers.py +++ b/certidude/helpers.py @@ -2,7 +2,6 @@ import click import os import requests -import urllib.request from certidude import errors from certidude.wrappers import Certificate, Request from OpenSSL import crypto @@ -123,12 +122,11 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path, return if submission.status_code == requests.codes.conflict: raise errors.DuplicateCommonNameError("Different signing request with same CN is already present on server, server refuses to overwrite") - else: - submission.raise_for_status() - - if submission.text == '-----BEGIN CERTIFICATE-----\n-----END CERTIFICATE-----\n': + elif submission.status_code == requests.codes.gone: # Should the client retry or disable request submission? raise ValueError("Server refused to sign the request") # TODO: Raise proper exception + else: + submission.raise_for_status() try: cert = crypto.load_certificate(crypto.FILETYPE_PEM, submission.text) diff --git a/certidude/push.py b/certidude/push.py index 52e0b7a..a06d4bf 100644 --- a/certidude/push.py +++ b/certidude/push.py @@ -1,7 +1,7 @@ import click import json -import urllib.request +import requests from certidude import config @@ -13,25 +13,9 @@ 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 - click.echo("Posting event %s %s at %s, waiting for response..." % (repr(event_type), repr(event_data), repr(url))) - notification = urllib.request.Request( - url, - event_data.encode("utf-8"), - {"X-EventSource-Event":event_type.encode("ascii")}) - notification.add_header("User-Agent", "Certidude API") - - try: - response = urllib.request.urlopen(notification) - body = response.read() - except urllib.error.HTTPError as err: - if err.code == 404: - print("No subscribers on the channel") - else: - print("Failed to submit event, %s" % err) - else: - print("Push server returned:", response.code, body) - response.close() - + notification = requests.post( + config.PUSH_PUBLISH % config.PUSH_TOKEN, + data=event_data, + headers={"X-EventSource-Event": event_type, "User-Agent": "Certidude API"}) diff --git a/certidude/wrappers.py b/certidude/wrappers.py index 5213d75..8f1da19 100644 --- a/certidude/wrappers.py +++ b/certidude/wrappers.py @@ -199,7 +199,7 @@ class Request(CertificateBase): self.path = NotImplemented self.created = NotImplemented - if isinstance(mixed, io.TextIOWrapper): + if isinstance(mixed, file): self.path = mixed.name _, _, _, _, _, _, _, _, mtime, _ = os.stat(self.path) self.created = datetime.fromtimestamp(mtime) @@ -248,7 +248,7 @@ class Certificate(CertificateBase): self.path = NotImplemented self.changed = NotImplemented - if isinstance(mixed, io.TextIOWrapper): + if isinstance(mixed, file): self.path = mixed.name _, _, _, _, _, _, _, _, mtime, _ = os.stat(self.path) self.changed = datetime.fromtimestamp(mtime) diff --git a/requirements.txt b/requirements.txt index 819f79b..1b2040a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ cffi==1.2.1 click==5.1 cryptography==1.0 falcon==0.3.0 +future=0.15.2 humanize==0.5.1 idna==2.0 ipsecparse==0.1.0