From 443c168cbdbc6a078a5f565f3e26d98ddfb5a598 Mon Sep 17 00:00:00 2001 From: Marvin Martinson Date: Thu, 19 Aug 2021 08:08:07 +0000 Subject: [PATCH] Add key curve hash to bootstrap, code style fixes --- pinecrypt/server/api/bootstrap.py | 5 +- pinecrypt/server/authority.py | 76 ++++++++++++++++++------------- pinecrypt/server/cli.py | 13 ++---- pinecrypt/server/const.py | 17 +++++-- 4 files changed, 67 insertions(+), 44 deletions(-) diff --git a/pinecrypt/server/api/bootstrap.py b/pinecrypt/server/api/bootstrap.py index 092449d..8b82a2d 100644 --- a/pinecrypt/server/api/bootstrap.py +++ b/pinecrypt/server/api/bootstrap.py @@ -1,5 +1,4 @@ import hashlib -import logging from pinecrypt.server import authority, const, config from pinecrypt.server.common import cert_to_dn from pinecrypt.server.decorators import serialize @@ -7,6 +6,7 @@ from pinecrypt.server.mongolog import LogHandler logger = LogHandler() + class BootstrapResource(object): @serialize def on_get(self, req, resp): @@ -31,6 +31,9 @@ class BootstrapResource(object): esp=config.get("Globals", "STRONGSWAN_ESP")["value"], ), certificate=dict( + key_size=const.KEY_SIZE, + curve=const.CURVE_NAME, + hash_algorithm=const.CERTIFICATE_HASH_ALGORITHM, algorithm=authority.public_key.algorithm, common_name=authority.certificate.subject.native["common_name"], distinguished_name=cert_to_dn(authority.certificate), diff --git a/pinecrypt/server/authority.py b/pinecrypt/server/authority.py index 4904aa3..009b299 100644 --- a/pinecrypt/server/authority.py +++ b/pinecrypt/server/authority.py @@ -1,5 +1,4 @@ import click -import logging import os import re import socket @@ -16,7 +15,7 @@ from csrbuilder import CSRBuilder, pem_armor_csr from datetime import datetime, timedelta from bson.objectid import ObjectId -#logger = logging.getLogger(__name__) +# logger = logging.getLogger(__name__) logger = LogHandler() # Cache CA certificate @@ -36,18 +35,18 @@ def self_enroll(skip_notify=False): common_name = const.HOSTNAME try: - cert, cert_doc, pem_buf = get_signed(common_name=common_name,namespace=const.AUTHORITY_NAMESPACE) + cert, cert_doc, pem_buf = get_signed(common_name=common_name, namespace=const.AUTHORITY_NAMESPACE) self_public_key = asymmetric.load_public_key(cert["tbs_certificate"]["subject_public_key_info"]) private_key = asymmetric.load_private_key(const.SELF_KEY_PATH) - except (NameError, FileNotFoundError, errors.CertificateDoesNotExist) as error: # certificate or private key not found + except (NameError, FileNotFoundError, errors.CertificateDoesNotExist): # certificate or private key not found click.echo("Generating private key for frontend: %s" % const.SELF_KEY_PATH) - with open(const.SELF_KEY_PATH, 'wb') as fh: + with open(const.SELF_KEY_PATH, "wb") as fh: if public_key.algorithm == "ec": self_public_key, private_key = asymmetric.generate_pair("ec", curve=public_key.curve) elif public_key.algorithm == "rsa": self_public_key, private_key = asymmetric.generate_pair("rsa", bit_size=public_key.bit_size) else: - raise NotImplemented("CA certificate public key algorithm %s not supported" % public_key.algorithm) + raise NotImplementedError("CA certificate public key algorithm %s not supported" % public_key.algorithm) fh.write(asymmetric.dump_private_key(private_key, None)) else: now = datetime.utcnow().replace(tzinfo=pytz.UTC) @@ -55,13 +54,13 @@ def self_enroll(skip_notify=False): click.echo("Self certificate still valid, delete to self-enroll again") return - builder = CSRBuilder({"common_name": common_name}, self_public_key) + builder.hash_algo = const.CERTIFICATE_HASH_ALGORITHM request = builder.build(private_key) now = datetime.utcnow().replace(tzinfo=pytz.UTC) - d ={} + d = {} d["submitted"] = now d["common_name"] = common_name d["request_buf"] = request.dump() @@ -69,13 +68,14 @@ def self_enroll(skip_notify=False): d["user"] = {} doc = db.certificates.find_one_and_update({ - "common_name":d["common_name"] + "common_name": d["common_name"] }, { "$set": d, "$setOnInsert": { "created": now, - "ip": [], - }}, + "ip": [] + } + }, upsert=True, return_document=db.return_new) @@ -98,6 +98,7 @@ def get_common_name_id(cn): return str(doc["_id"]) + def list_revoked(limit=0): # TODO: sort recent to oldest for cert_revoked_doc in db.certificates.find({"status": "revoked"}): @@ -108,9 +109,10 @@ def list_revoked(limit=0): if limit <= 0: return + # TODO: it should be possible to regex search common_name directly from mongodb def list_signed(common_name=None): - for cert_doc in db.certificates.find({"status" : "signed"}): + for cert_doc in db.certificates.find({"status": "signed"}): if common_name: if common_name.startswith("^"): if not re.match(common_name, cert_doc["common_name"]): @@ -121,24 +123,28 @@ def list_signed(common_name=None): cert = x509.Certificate.load(cert_doc["cert_buf"]) yield cert_doc, cert + def list_requests(): for request in db.certificates.find({"status": "csr"}): csr = CertificationRequest.load(request["request_buf"]) yield csr, request, "." in request["common_name"] + def list_replicas(): """ Return list of Mongo objects referring to all active replicas """ - for doc in db.certificates.find({"status" : "signed", "profile.ou": "Gateway"}): + for doc in db.certificates.find({"status": "signed", "profile.ou": "Gateway"}): yield doc + def get_ca_cert(): fh = open(const.AUTHORITY_CERTIFICATE_PATH, "rb") server_certificate = asymmetric.load_certificate(fh.read()) fh.close() return server_certificate + def get_request(id): if not id: raise ValueError("Invalid id parameter %s" % id) @@ -146,11 +152,12 @@ def get_request(id): csr_doc = db.certificates.find_one({"_id": ObjectId(id), "status": "csr"}) if not csr_doc: - raise errors.RequestDoesNotExist("Certificate signing request with id %s does not exist" % id) + raise errors.RequestDoesNotExist("Certificate signing request with id %s does not exist" % id) csr = CertificationRequest.load(csr_doc["request_buf"]) return csr, csr_doc, pem_armor_csr(csr) + def get_by_serial(serial): serial_string = "%x" % serial query = {"serial_number": serial_string} @@ -163,6 +170,7 @@ def get_by_serial(serial): cert = x509.Certificate.load(cert_doc["cert_buf"]) return cert_doc, cert + def get_signed(mongo_id=False, common_name=False, namespace=const.AUTHORITY_NAMESPACE): if mongo_id: @@ -189,13 +197,13 @@ def get_revoked(serial): if isinstance(serial, int): serial = "%x" % serial - query = {"serial_number":serial, "status": "revoked"} + query = {"serial_number": serial, "status": "revoked"} cert_doc = db.certificates.find_one(query) if not cert_doc: raise errors.CertificateDoesNotExist - cert_pem_buf = pem.armor("CERTIFICATE",cert_doc["cert_buf"]) + cert_pem_buf = pem.armor("CERTIFICATE", cert_doc["cert_buf"]) return cert_doc, cert_pem_buf @@ -221,10 +229,9 @@ def store_request(buf, overwrite=False, address="", user="", namespace=const.MAC if not re.match(const.RE_COMMON_NAME, common_name): raise ValueError("Invalid common name %s" % repr(common_name)) - query = {"common_name": common_name, "status": "csr"} doc = db.certificates.find_one(query) - d ={} + d = {} user_object = {} if doc and not overwrite: @@ -255,7 +262,7 @@ def store_request(buf, overwrite=False, address="", user="", namespace=const.MAC d["user"] = user_object doc = db.certificates.find_one_and_update({ - "common_name":d["common_name"] + "common_name": d["common_name"] }, { "$set": d, "$setOnInsert": { @@ -267,6 +274,7 @@ def store_request(buf, overwrite=False, address="", user="", namespace=const.MAC return doc + def revoke(mongo_id, reason, user="root"): """ Revoke valid certificate @@ -275,8 +283,8 @@ def revoke(mongo_id, reason, user="root"): common_name = cert_doc["common_name"] if reason not in ("key_compromise", "ca_compromise", "affiliation_changed", - "superseded", "cessation_of_operation", "certificate_hold", - "remove_from_crl", "privilege_withdrawn"): + "superseded", "cessation_of_operation", "certificate_hold", + "remove_from_crl", "privilege_withdrawn"): raise ValueError("Invalid revocation reason %s" % reason) logger.info("Revoked certificate %s by %s", common_name, user) @@ -288,9 +296,9 @@ def revoke(mongo_id, reason, user="root"): else: raise ValueError("No common name or Id specified") - prev = db.certificates.find_one(query) - newValue = { "$set": { "status": "revoked", "revocation_reason": reason, "revoked": datetime.utcnow().replace(tzinfo=pytz.UTC)} } - db.certificates.find_one_and_update(query,newValue) + # prev = db.certificates.find_one(query) + newValue = {"$set": {"status": "revoked", "revocation_reason": reason, "revoked": datetime.utcnow().replace(tzinfo=pytz.UTC)}} + db.certificates.find_one_and_update(query, newValue) attach_cert = pem_buf, "application/x-pem-file", common_name + ".crt" @@ -299,6 +307,7 @@ def revoke(mongo_id, reason, user="root"): serial_hex="%x" % cert.serial_number, common_name=common_name) + def export_crl(pem=True): builder = CertificateListBuilder( const.AUTHORITY_CRL_URL, @@ -309,7 +318,7 @@ def export_crl(pem=True): # Get revoked certificates from database for cert_revoked_doc in db.certificates.find({"status": "revoked"}): builder.add_certificate( - int(cert_revoked_doc["serial"][:-4],16), + int(cert_revoked_doc["serial"][:-4], 16), datetime.utcfromtimestamp(cert_revoked_doc["revoked"]).replace(tzinfo=pytz.UTC), cert_revoked_doc["revocation_reason"] ) @@ -331,10 +340,11 @@ def delete_request(id, user="root"): if not doc: logger.info("Signing request with id %s not found" % ( - id)) + id)) raise errors.RequestDoesNotExist - res = db.certificates.delete_one(query) + # TODO return if was success or failure + db.certificates.delete_one(query) logger.info("Rejected signing request %s %s by %s" % (doc["common_name"], id, user)) @@ -345,11 +355,10 @@ def sign(profile, skip_notify=False, overwrite=False, signer=None, namespace=con if mongo_id: csr_doc = db.certificates.find_one({"_id": ObjectId(mongo_id)}) csr = CertificationRequest.load(csr_doc["request_buf"]) - csr_buf_pem = pem.armor("CERTIFICATE REQUEST",csr_doc["request_buf"]) + csr_buf_pem = pem.armor("CERTIFICATE REQUEST", csr_doc["request_buf"]) else: raise ValueError("ID missing, what CSR to sign") - assert isinstance(csr, CertificationRequest) csr_pubkey = asymmetric.load_public_key(csr["certification_request_info"]["subject_pk_info"]) @@ -371,8 +380,8 @@ def sign(profile, skip_notify=False, overwrite=False, signer=None, namespace=con if prev: if overwrite: - newValue = { "$set": { "status": "revoked", "revoked": datetime.utcnow().replace(tzinfo=pytz.UTC), "revocation_reason": "superseded"} } - doc = db.certificates.find_one_and_update(query,newValue,return_document=db.return_new) + newValue = {"$set": {"status": "revoked", "revoked": datetime.utcnow().replace(tzinfo=pytz.UTC), "revocation_reason": "superseded"}} + doc = db.certificates.find_one_and_update(query, newValue, return_document=db.return_new) overwritten = True else: raise FileExistsError("Will not overwrite existing certificate") @@ -382,6 +391,9 @@ def sign(profile, skip_notify=False, overwrite=False, signer=None, namespace=con ou=profile["ou"]), csr_pubkey) builder.serial_number = generate_serial() + if csr["signature_algorithm"].hash_algo == const.CERTIFICATE_HASH_ALGORITHM: + builder.hash_algo = const.CERTIFICATE_HASH_ALGORITHM + now = datetime.utcnow().replace(tzinfo=pytz.UTC) builder.begin_date = now - const.CLOCK_SKEW_TOLERANCE builder.end_date = now + timedelta(days=profile["lifetime"]) @@ -408,7 +420,7 @@ def sign(profile, skip_notify=False, overwrite=False, signer=None, namespace=con # Write certificate to database # DER format cert - cert_der_bytes = asymmetric.dump_certificate(end_entity_cert,encoding="der") + cert_der_bytes = asymmetric.dump_certificate(end_entity_cert, encoding="der") d = { "common_name": common_name, diff --git a/pinecrypt/server/cli.py b/pinecrypt/server/cli.py index 99bf209..3f758b4 100644 --- a/pinecrypt/server/cli.py +++ b/pinecrypt/server/cli.py @@ -10,7 +10,6 @@ else: import falcon import click -import logging import os import pymongo import signal @@ -27,14 +26,12 @@ from pinecrypt.server import const, mongolog, mailer, db from pinecrypt.server.middleware import NormalizeMiddleware, PrometheusEndpoint from pinecrypt.server.common import cn_to_dn, generate_serial from pinecrypt.server.mongolog import LogHandler -#from pinecrypt.server.logger import CertidudeLogger from time import sleep from wsgiref.simple_server import make_server -#logger = logging.getLogger(__name__) -#logger = CertidudeLogger() logger = LogHandler() + def graceful_exit(signal_number, stack_frame): print("Received signal %d, exiting now" % signal_number) sys.exit(0) @@ -218,7 +215,7 @@ def pinecone_serve_builder(): @click.command("provision", help="Provision keys") def pinecone_provision(): - #First thing init mongo db + # First thing init mongo db click.echo("Provisioning MongoDB replicaset") # WTF https://github.com/docker-library/mongo/issues/339 c = pymongo.MongoClient("localhost", 27017) @@ -236,9 +233,8 @@ def pinecone_provision(): config = {"_id": "rs0", "members": [ {"_id": index, "host": "%s:%s" % (ip_port[0], ip_port[1])} for index, ip_port in enumerate(mongo_uri["nodelist"])]} - - # config = {"_id":"rs0", "members": [ - # {"_id": 0, "host": "127.0.0.1:27017"}]} + # config = {"_id":"rs0", "members": [ + # {"_id": 0, "host": "127.0.0.1:27017"}]} print("Provisioning MongoDB replicaset: %s" % repr(config)) try: @@ -263,6 +259,7 @@ def pinecone_provision(): # https://technet.microsoft.com/en-us/library/aa998840(v=exchg.141).aspx builder = CertificateBuilder(distinguished_name, public_key) + builder.hash_algo = const.CERTIFICATE_HASH_ALGORITHM builder.self_signed = True builder.ca = True builder.serial_number = generate_serial() diff --git a/pinecrypt/server/const.py b/pinecrypt/server/const.py index ab74452..c68772b 100644 --- a/pinecrypt/server/const.py +++ b/pinecrypt/server/const.py @@ -35,8 +35,13 @@ REPLICAS = [j for j in os.getenv("REPLICAS", "").split(",") if j] if not MONGO_URI: MONGO_URI = "mongodb://127.0.0.1:27017/default?replicaSet=rs0" -KEY_SIZE = 4096 -CURVE_NAME = "secp384r1" +# Are set later, based on key type +KEY_SIZE = None +CURVE_NAME = None + +# python CSRbuilder supports right now sha1, sha256 sha512 +# CERTIFICATE_HASH_ALGORITHM = 'sha111' +CERTIFICATE_HASH_ALGORITHM = "sha512" # Kerberos-like clock skew tolerance CLOCK_SKEW_TOLERANCE = timedelta(minutes=5) @@ -76,7 +81,7 @@ AUTHORITY_ORGANIZATION = os.getenv("AUTHORITY_ORGANIZATION") AUTHORITY_LIFETIME_DAYS = 20 * 365 # Advertise following IP addresses via DNS record -ADVERTISE_ADDRESS = [j for j in os.getenv("ADVERTISE_ADDRESS", "").split(",") if j] +ADVERTISE_ADDRESS = [j for j in os.getenv("ADVERTISE_ADDRESS", "").split(",") if j] if not ADVERTISE_ADDRESS: ADVERTISE_ADDRESS = set() for fam, _, _, _, addrs in socket.getaddrinfo(FQDN, None): @@ -100,6 +105,12 @@ AUTHORITY_OCSP_URL = "http://%s/api/ocsp/" % AUTHORITY_NAMESPACE AUTHORITY_OCSP_DISABLED = os.getenv("AUTHORITY_OCSP_DISABLED", False) AUTHORITY_KEYTYPE = getenv_in("AUTHORITY_KEYTYPE", "rsa", "ec") +if AUTHORITY_KEYTYPE == "rsa": + KEY_SIZE = 4096 + +if AUTHORITY_KEYTYPE == "ec": + CURVE_NAME = "secp384r1" + # Tokens TOKEN_URL = "https://%(authority_name)s/#action=enroll&title=dev.lan&token=%(token)s&subject=%(subject_username)s&protocols=%(protocols)s" TOKEN_LIFETIME = 3600 * 24