mirror of
				https://github.com/laurivosandi/certidude
				synced 2025-10-31 01:19:11 +00:00 
			
		
		
		
	Migrate from cryptography.io to oscrypto
This commit is contained in:
		
							
								
								
									
										27
									
								
								README.rst
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								README.rst
									
									
									
									
									
								
							| @@ -11,9 +11,8 @@ Certidude | ||||
| Introduction | ||||
| ------------ | ||||
|  | ||||
| Certidude is a novel X.509 Certificate Authority management tool | ||||
| with privilege isolation mechanism and Kerberos authentication | ||||
| mainly designed for OpenVPN gateway operators to make | ||||
| Certidude is a minimalist X.509 Certificate Authority management tool | ||||
| with Kerberos authentication mainly designed for OpenVPN gateway operators to make | ||||
| VPN client setup on laptops, desktops and mobile devices as painless as possible. | ||||
|  | ||||
| .. figure:: doc/certidude.png | ||||
| @@ -54,13 +53,6 @@ Following usecases are covered: | ||||
|   The user logs in using domain account in the web interface and can automatically | ||||
|   retrieve a P12 bundle which can be installed on her Android device. | ||||
|  | ||||
| Future usecases: | ||||
|  | ||||
| * I want to store the private key of my CA on a SmartCard. | ||||
|   I want to make use of it while I log in to my CA web interface. | ||||
|   When I am asked to sign a certificate I have to enter PIN code to unlock the | ||||
|   SmartCard. | ||||
|  | ||||
|  | ||||
| Features | ||||
| -------- | ||||
| @@ -68,16 +60,14 @@ Features | ||||
| Common: | ||||
|  | ||||
| * Standard request, sign, revoke workflow via web interface. | ||||
| * Kerberos and basic auth based web interface authentication. | ||||
| * Preliminary `OCSP <https://tools.ietf.org/html/rfc4557>`_ and `SCEP <https://tools.ietf.org/html/draft-nourse-scep-23>`_ support. | ||||
| * `OCSP <https://tools.ietf.org/html/rfc4557>`_ and `SCEP <https://tools.ietf.org/html/draft-nourse-scep-23>`_ support. | ||||
| * PAM and Active Directory compliant authentication backends: Kerberos single sign-on, LDAP simple bind. | ||||
| * POSIX groups and Active Directory (LDAP) group membership based authorization. | ||||
| * Server-side command-line interface, check out ``certidude list``, ``certidude sign`` and ``certidude revoke``. | ||||
| * Privilege isolation, separate signer process is spawned per private key isolating | ||||
|   private key use from the the web interface. | ||||
| * Certificate serial numbers are intentionally randomized to avoid leaking information about business practices. | ||||
| * Server-side events support via `nchan <https://nchan.slact.net/>`_. | ||||
| * E-mail notifications about pending, signed, revoked, renewed and overwritten certificates | ||||
| * E-mail notifications about pending, signed, revoked, renewed and overwritten certificates. | ||||
| * Built using compilation-free `oscrypto <https://github.com/wbond/oscrypto>`_ library. | ||||
|  | ||||
| Virtual private networking: | ||||
|  | ||||
| @@ -95,9 +85,7 @@ HTTPS: | ||||
| TODO | ||||
| ---- | ||||
|  | ||||
| * WebCrypto support, meanwhile check out `hwcrypto.js <https://github.com/open-eid/hwcrypto.js>`_. | ||||
| * Use `pki.js <https://pkijs.org/>`_ for generating keypair in the browser when claiming a token. | ||||
| * Signer process logging. | ||||
|  | ||||
|  | ||||
| Install | ||||
| @@ -110,7 +98,8 @@ System dependencies for Ubuntu 16.04: | ||||
|  | ||||
| .. code:: bash | ||||
|  | ||||
|     apt install -y python python-cffi python-click python-configparser \ | ||||
|     apt install -y | ||||
|         python-click python-configparser \ | ||||
|         python-humanize \ | ||||
|         python-ipaddress python-jinja2 python-ldap python-markdown \ | ||||
|         python-mimeparse python-mysql.connector python-openssl python-pip \ | ||||
| @@ -124,7 +113,7 @@ System dependencies for Fedora 25+: | ||||
|     yum install redhat-rpm-config python-devel openssl-devel openldap-devel | ||||
|  | ||||
| At the moment package at PyPI is rather outdated. | ||||
| Please proceed down to Development section to install Certidude from source. | ||||
| Please proceed down to `Development <#development>`_ section to install Certidude from source. | ||||
|  | ||||
|  | ||||
| Setting up authority | ||||
|   | ||||
| @@ -13,7 +13,6 @@ from certidude import authority, mailer | ||||
| from certidude.auth import login_required, authorize_admin | ||||
| from certidude.user import User | ||||
| from certidude.decorators import serialize, csrf_protection | ||||
| from cryptography.x509.oid import NameOID | ||||
| from certidude import const, config | ||||
|  | ||||
| logger = logging.getLogger(__name__) | ||||
| @@ -82,8 +81,8 @@ class SessionResource(object): | ||||
|                     common_name = common_name, | ||||
|                     server = server, | ||||
|                     # TODO: key type, key length, key exponent, key modulo | ||||
|                     signed = obj.not_valid_before, | ||||
|                     expires = obj.not_valid_after, | ||||
|                     signed = obj["tbs_certificate"]["validity"]["not_before"].native, | ||||
|                     expires = obj["tbs_certificate"]["validity"]["not_after"].native, | ||||
|                     sha256sum = hashlib.sha256(buf).hexdigest(), | ||||
|                     lease = lease, | ||||
|                     tags = tags, | ||||
| @@ -108,8 +107,7 @@ class SessionResource(object): | ||||
|                     offline = 600, # Seconds from last seen activity to consider lease offline, OpenVPN reneg-sec option | ||||
|                     dead = 604800 # Seconds from last activity to consider lease dead, X509 chain broken or machine discarded | ||||
|                 ), | ||||
|                 common_name = authority.ca_cert.subject.get_attributes_for_oid( | ||||
|                     NameOID.COMMON_NAME)[0].value, | ||||
|                 common_name = authority.certificate.subject.native["common_name"], | ||||
|                 mailer = dict( | ||||
|                     name = config.MAILER_NAME, | ||||
|                     address = config.MAILER_ADDRESS | ||||
|   | ||||
| @@ -33,7 +33,7 @@ class LeaseResource(object): | ||||
|         # TODO: verify signature | ||||
|         common_name = req.get_param("client", required=True) | ||||
|         path, buf, cert = authority.get_signed(common_name) # TODO: catch exceptions | ||||
|         if req.get_param("serial") and cert.serial != req.get_param_as_int("serial"): # OCSP-ish solution for OpenVPN, not exposed for StrongSwan | ||||
|         if req.get_param("serial") and cert.serial_number != req.get_param_as_int("serial"): # OCSP-ish solution for OpenVPN, not exposed for StrongSwan | ||||
|             raise falcon.HTTPForbidden("Forbidden", "Invalid serial number supplied") | ||||
|  | ||||
|         xattr.setxattr(path, "user.lease.outer_address", req.get_param("outer_address", required=True).encode("ascii")) | ||||
|   | ||||
| @@ -52,7 +52,7 @@ class OCSPResource(object): | ||||
|                 assert link_target.startswith("../") | ||||
|                 assert link_target.endswith(".pem") | ||||
|                 path, buf, cert = authority.get_signed(link_target[3:-4]) | ||||
|                 if serial != cert.serial: | ||||
|                 if serial != cert.serial_number: | ||||
|                     raise EnvironmentError("integrity check failed") | ||||
|                 status = ocsp.CertStatus(name='good', value=None) | ||||
|             except EnvironmentError: | ||||
| @@ -94,9 +94,13 @@ class OCSPResource(object): | ||||
|                 'response_type': u"basic_ocsp_response", | ||||
|                 'response': { | ||||
|                     'tbs_response_data': response_data, | ||||
|                     'certs': [server_certificate.asn1], | ||||
|                     'signature_algorithm': {'algorithm': u"sha1_rsa"}, | ||||
|                     'signature': b64decode(authority.signer_exec("sign-pkcs7", b64encode(response_data.dump()))), | ||||
|                     'certs': [server_certificate.asn1] | ||||
|                     'signature': asymmetric.rsa_pkcs1v15_sign( | ||||
|                         authority.private_key, | ||||
|                         response_data.dump(), | ||||
|                         "sha1" | ||||
|                     ) | ||||
|                 } | ||||
|             } | ||||
|         }).dump() | ||||
|   | ||||
| @@ -6,18 +6,16 @@ import ipaddress | ||||
| import json | ||||
| import os | ||||
| import hashlib | ||||
| from asn1crypto import pem | ||||
| from asn1crypto.csr import CertificationRequest | ||||
| from base64 import b64decode | ||||
| from certidude import config, authority, push, errors | ||||
| from certidude.auth import login_required, login_optional, authorize_admin | ||||
| from certidude.decorators import serialize, csrf_protection | ||||
| from certidude.firewall import whitelist_subnets, whitelist_content_types | ||||
| from cryptography import x509 | ||||
| from cryptography.hazmat.backends import default_backend | ||||
| from cryptography.hazmat.primitives import hashes | ||||
| from cryptography.hazmat.primitives.asymmetric import padding | ||||
| from cryptography.exceptions import InvalidSignature | ||||
| from cryptography.x509.oid import NameOID | ||||
| from datetime import datetime | ||||
| from oscrypto import asymmetric | ||||
| from oscrypto.errors import SignatureError | ||||
| from xattr import getxattr | ||||
|  | ||||
| logger = logging.getLogger(__name__) | ||||
| @@ -35,19 +33,14 @@ class RequestListResource(object): | ||||
|     @whitelist_content_types("application/pkcs10") | ||||
|     def on_post(self, req, resp): | ||||
|         """ | ||||
|         Validate and parse certificate signing request | ||||
|         Validate and parse certificate signing request, the RESTful way | ||||
|         """ | ||||
|         reasons = [] | ||||
|         body = req.stream.read(req.content_length) | ||||
|         csr = x509.load_pem_x509_csr(body, default_backend()) | ||||
|         try: | ||||
|             common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME) | ||||
|         except: # ValueError? | ||||
|             logger.warning(u"Rejected signing request without common name from %s", | ||||
|                 req.context.get("remote_addr")) | ||||
|             raise falcon.HTTPBadRequest( | ||||
|                 "Bad request", | ||||
|                 "No common name specified!") | ||||
|         body = req.stream.read(req.content_length).encode("ascii") | ||||
|  | ||||
|         header, _, der_bytes = pem.unarmor(body) | ||||
|         csr = CertificationRequest.load(der_bytes) | ||||
|         common_name = csr["certification_request_info"]["subject"].native["common_name"] | ||||
|  | ||||
|         """ | ||||
|         Handle domain computer automatic enrollment | ||||
| @@ -55,10 +48,10 @@ class RequestListResource(object): | ||||
|         machine = req.context.get("machine") | ||||
|         if machine: | ||||
|             if config.MACHINE_ENROLLMENT_ALLOWED: | ||||
|                 if common_name.value != machine: | ||||
|                 if common_name != machine: | ||||
|                     raise falcon.HTTPBadRequest( | ||||
|                         "Bad request", | ||||
|                         "Common name %s differs from Kerberos credential %s!" % (common_name.value, machine)) | ||||
|                         "Common name %s differs from Kerberos credential %s!" % (common_name, machine)) | ||||
|  | ||||
|                 # Automatic enroll with Kerberos machine cerdentials | ||||
|                 resp.set_header("Content-Type", "application/x-pem-file") | ||||
| @@ -73,52 +66,48 @@ class RequestListResource(object): | ||||
|         Attempt to renew certificate using currently valid key pair | ||||
|         """ | ||||
|         try: | ||||
|             path, buf, cert = authority.get_signed(common_name.value) | ||||
|             path, buf, cert = authority.get_signed(common_name) | ||||
|         except EnvironmentError: | ||||
|             pass | ||||
|             pass # No currently valid certificate for this common name | ||||
|         else: | ||||
|             if cert.public_key().public_numbers() == csr.public_key().public_numbers(): | ||||
|             cert_pk = cert["tbs_certificate"]["subject_public_key_info"].native | ||||
|             csr_pk = csr["certification_request_info"]["subject_pk_info"].native | ||||
|  | ||||
|             if cert_pk == csr_pk: # Same public key, assume renewal | ||||
|                 expires = cert["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None) | ||||
|                 renewal_header = req.get_header("X-Renewal-Signature") | ||||
|  | ||||
|                 if not renewal_header: | ||||
|                     # No header supplied, redirect to signed API call | ||||
|                     resp.status = falcon.HTTP_SEE_OTHER | ||||
|                     resp.location = os.path.join(os.path.dirname(req.relative_uri), "signed", common_name.value) | ||||
|                     resp.location = os.path.join(os.path.dirname(req.relative_uri), "signed", common_name) | ||||
|                     return | ||||
|  | ||||
|                 try: | ||||
|                     renewal_signature = b64decode(renewal_header) | ||||
|                 except TypeError, ValueError: | ||||
|                     logger.error(u"Renewal failed, bad signature supplied for %s", common_name.value) | ||||
|                     logger.error(u"Renewal failed, bad signature supplied for %s", common_name) | ||||
|                     reasons.append("Renewal failed, bad signature supplied") | ||||
|                 else: | ||||
|                     try: | ||||
|                         verifier = cert.public_key().verifier( | ||||
|                             renewal_signature, | ||||
|                             padding.PSS( | ||||
|                                 mgf=padding.MGF1(hashes.SHA512()), | ||||
|                                 salt_length=padding.PSS.MAX_LENGTH | ||||
|                             ), | ||||
|                             hashes.SHA512() | ||||
|                         ) | ||||
|                         verifier.update(buf) | ||||
|                         verifier.update(body) | ||||
|                         verifier.verify() | ||||
|                     except InvalidSignature: | ||||
|                         logger.error(u"Renewal failed, invalid signature supplied for %s", common_name.value) | ||||
|                         asymmetric.rsa_pss_verify( | ||||
|                             asymmetric.load_certificate(cert), | ||||
|                             renewal_signature, buf + body, "sha512") | ||||
|                     except SignatureError: | ||||
|                         logger.error(u"Renewal failed, invalid signature supplied for %s", common_name) | ||||
|                         reasons.append("Renewal failed, invalid signature supplied") | ||||
|                     else: | ||||
|                         # At this point renewal signature was valid but we need to perform some extra checks | ||||
|                         if datetime.utcnow() > cert.not_valid_after: | ||||
|                             logger.error(u"Renewal failed, current certificate for %s has expired", common_name.value) | ||||
|                         if datetime.utcnow() > expires: | ||||
|                             logger.error(u"Renewal failed, current certificate for %s has expired", common_name) | ||||
|                             reasons.append("Renewal failed, current certificate expired") | ||||
|                         elif not config.CERTIFICATE_RENEWAL_ALLOWED: | ||||
|                             logger.error(u"Renewal requested for %s, but not allowed by authority settings", common_name.value) | ||||
|                             logger.error(u"Renewal requested for %s, but not allowed by authority settings", common_name) | ||||
|                             reasons.append("Renewal requested, but not allowed by authority settings") | ||||
|                         else: | ||||
|                             resp.set_header("Content-Type", "application/x-x509-user-cert") | ||||
|                             _, resp.body = authority._sign(csr, body, overwrite=True) | ||||
|                             logger.info(u"Renewed certificate for %s", common_name.value) | ||||
|                             logger.info(u"Renewed certificate for %s", common_name) | ||||
|                             return | ||||
|  | ||||
|  | ||||
| @@ -127,17 +116,17 @@ class RequestListResource(object): | ||||
|         autosigning was requested and certificate can be automatically signed | ||||
|         """ | ||||
|         if req.get_param_as_bool("autosign"): | ||||
|             if "." not in common_name.value: | ||||
|             if not authority.server_flags(common_name): | ||||
|                 for subnet in config.AUTOSIGN_SUBNETS: | ||||
|                     if req.context.get("remote_addr") in subnet: | ||||
|                         try: | ||||
|                             resp.set_header("Content-Type", "application/x-pem-file") | ||||
|                             _, resp.body = authority._sign(csr, body) | ||||
|                             logger.info(u"Autosigned %s as %s is whitelisted", common_name.value, req.context.get("remote_addr")) | ||||
|                             logger.info(u"Autosigned %s as %s is whitelisted", common_name, req.context.get("remote_addr")) | ||||
|                             return | ||||
|                         except EnvironmentError: | ||||
|                             logger.info(u"Autosign for %s from %s failed, signed certificate already exists", | ||||
|                                 common_name.value, req.context.get("remote_addr")) | ||||
|                                 common_name, req.context.get("remote_addr")) | ||||
|                             reasons.append("Autosign failed, signed certificate already exists") | ||||
|                         break | ||||
|                 else: | ||||
| @@ -147,7 +136,7 @@ class RequestListResource(object): | ||||
|  | ||||
|         # Attempt to save the request otherwise | ||||
|         try: | ||||
|             request_path, _, _ = authority.store_request(body.decode("ascii"), | ||||
|             request_path, _, _ = authority.store_request(body, | ||||
|                 address=str(req.context.get("remote_addr"))) | ||||
|         except errors.RequestExists: | ||||
|             reasons.append("Same request already uploaded exists") | ||||
| @@ -160,10 +149,10 @@ class RequestListResource(object): | ||||
|                 "CSR with such CN already exists", | ||||
|                 "Will not overwrite existing certificate signing request, explicitly delete CSR and try again") | ||||
|         else: | ||||
|             push.publish("request-submitted", common_name.value) | ||||
|             push.publish("request-submitted", common_name) | ||||
|  | ||||
|         # Wait the certificate to be signed if waiting is requested | ||||
|         logger.info(u"Signing request %s from %s stored", common_name.value, req.context.get("remote_addr")) | ||||
|         logger.info(u"Stored signing request %s from %s", common_name, req.context.get("remote_addr")) | ||||
|         if req.get_param("wait"): | ||||
|             # Redirect to nginx pub/sub | ||||
|             url = config.LONG_POLL_SUBSCRIBE % hashlib.sha256(body).hexdigest() | ||||
| @@ -221,7 +210,7 @@ class RequestDetailResource(object): | ||||
|     @csrf_protection | ||||
|     @login_required | ||||
|     @authorize_admin | ||||
|     def on_patch(self, req, resp, cn): | ||||
|     def on_post(self, req, resp, cn): | ||||
|         """ | ||||
|         Sign a certificate signing request | ||||
|         """ | ||||
|   | ||||
| @@ -6,9 +6,6 @@ import logging | ||||
| from certidude import const, config | ||||
| from certidude.authority import export_crl, list_revoked | ||||
| from certidude.firewall import whitelist_subnets | ||||
| from cryptography import x509 | ||||
| from cryptography.hazmat.backends import default_backend | ||||
| from cryptography.hazmat.primitives.serialization import Encoding | ||||
|  | ||||
| logger = logging.getLogger(__name__) | ||||
|  | ||||
| @@ -23,9 +20,8 @@ class RevocationListResource(object): | ||||
|                 "Content-Disposition", | ||||
|                 ("attachment; filename=%s.crl" % const.HOSTNAME).encode("ascii")) | ||||
|             # Convert PEM to DER | ||||
|             logger.debug(u"Serving revocation list to %s in DER format", req.context.get("remote_addr")) | ||||
|             resp.body = x509.load_pem_x509_crl(export_crl(), | ||||
|                 default_backend()).public_bytes(Encoding.DER) | ||||
|             logger.debug(u"Serving revocation list (DER) to %s", req.context.get("remote_addr")) | ||||
|             resp.body = export_crl(pem=False) | ||||
|         elif req.client_accepts("application/x-pem-file"): | ||||
|             if req.get_param_as_bool("wait"): | ||||
|                 url = config.LONG_POLL_SUBSCRIBE % "crl" | ||||
| @@ -38,7 +34,7 @@ class RevocationListResource(object): | ||||
|                 resp.append_header( | ||||
|                     "Content-Disposition", | ||||
|                     ("attachment; filename=%s-crl.pem" % const.HOSTNAME).encode("ascii")) | ||||
|                 logger.debug(u"Serving revocation list to %s in PEM format", req.context.get("remote_addr")) | ||||
|                 logger.debug(u"Serving revocation list (PEM) to %s", req.context.get("remote_addr")) | ||||
|                 resp.body = export_crl() | ||||
|         else: | ||||
|             logger.debug(u"Client %s asked revocation list in unsupported format" % req.context.get("remote_addr")) | ||||
|   | ||||
| @@ -41,7 +41,7 @@ class SCEPResource(object): | ||||
|     def on_get(self, req, resp): | ||||
|         operation = req.get_param("operation") | ||||
|         if operation.lower() == "getcacert": | ||||
|             resp.stream = keys.parse_certificate(authority.ca_buf).dump() | ||||
|             resp.stream = keys.parse_certificate(authority.certificate_buf).dump() | ||||
|             resp.append_header("Content-Type", "application/x-x509-ca-cert") | ||||
|             return | ||||
|  | ||||
| @@ -125,14 +125,16 @@ class SCEPResource(object): | ||||
|             encrypted_content = encrypted_content_info['encrypted_content'].native | ||||
|             recipient, = encrypted_envelope['recipient_infos'] | ||||
|  | ||||
|             if recipient.native["rid"]["serial_number"] != authority.ca_cert.serial: | ||||
|             if recipient.native["rid"]["serial_number"] != authority.certificate.serial_number: | ||||
|                 raise SCEPBadCertId() | ||||
|  | ||||
|             # Since CA private key is not directly readable here, we'll redirect it to signer socket | ||||
|             key = b64decode(authority.signer_exec("decrypt-pkcs7", b64encode(recipient.native["encrypted_key"]))) | ||||
|             key = asymmetric.rsa_pkcs1v15_decrypt( | ||||
|                 authority.private_key, | ||||
|                 recipient.native["encrypted_key"]) | ||||
|             if len(key) == 8: key = key * 3 # Convert DES to 3DES | ||||
|             buf = symmetric.tripledes_cbc_pkcs5_decrypt(key, encrypted_content, iv) | ||||
|             _, common_name = authority.store_request(buf, overwrite=True) | ||||
|             _, _, common_name = authority.store_request(buf, overwrite=True) | ||||
|             cert, buf = authority.sign(common_name, overwrite=True) | ||||
|             signed_certificate = asymmetric.load_certificate(buf) | ||||
|             content = signed_certificate.asn1.dump() | ||||
| @@ -251,7 +253,11 @@ class SCEPResource(object): | ||||
|                 }), | ||||
|                 'digest_algorithm': algos.DigestAlgorithm({'algorithm': u"sha1"}), | ||||
|                 'signature_algorithm': algos.SignedDigestAlgorithm({'algorithm': u"rsassa_pkcs1v15"}), | ||||
|                 'signature': b64decode(authority.signer_exec("sign-pkcs7", b64encode(b"\x31" + attrs.dump()[1:]))) | ||||
|                 'signature': asymmetric.rsa_pkcs1v15_sign( | ||||
|                     authority.private_key, | ||||
|                     b"\x31" + attrs.dump()[1:], | ||||
|                     "sha1" | ||||
|                 ) | ||||
|             }) | ||||
|  | ||||
|             resp.append_header("Content-Type", "application/x-pki-message") | ||||
|   | ||||
| @@ -31,9 +31,9 @@ class SignedCertificateDetailResource(object): | ||||
|             resp.set_header("Content-Disposition", ("attachment; filename=%s.json" % cn)) | ||||
|             resp.body = json.dumps(dict( | ||||
|                 common_name = cn, | ||||
|                 serial_number = "%x" % cert.serial, | ||||
|                 signed = cert.not_valid_before.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z", | ||||
|                 expires = cert.not_valid_after.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z", | ||||
|                 serial_number = "%x" % cert.serial_number, | ||||
|                 signed = cert["tbs_certificate"]["validity"]["not_before"].native.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z", | ||||
|                 expires = cert["tbs_certificate"]["validity"]["not_after"].native.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z", | ||||
|                 sha256sum = hashlib.sha256(buf).hexdigest())) | ||||
|             logger.debug(u"Served certificate %s to %s as application/json", | ||||
|                 cn, req.context.get("remote_addr")) | ||||
|   | ||||
| @@ -1,23 +1,25 @@ | ||||
| from __future__ import division, absolute_import, print_function | ||||
| import click | ||||
| import os | ||||
| import random | ||||
| import re | ||||
| import requests | ||||
| import hashlib | ||||
| import socket | ||||
| from datetime import datetime, timedelta | ||||
| from cryptography.hazmat.backends import default_backend | ||||
| from cryptography import x509 | ||||
| from cryptography.x509.oid import NameOID, ExtensionOID, ExtendedKeyUsageOID | ||||
| from cryptography.hazmat.primitives.asymmetric import rsa | ||||
| from cryptography.hazmat.primitives import hashes, serialization | ||||
| from cryptography.hazmat.primitives.serialization import Encoding | ||||
| from oscrypto import asymmetric | ||||
| from asn1crypto import pem, x509 | ||||
| from asn1crypto.csr import CertificationRequest | ||||
| from certbuilder import CertificateBuilder | ||||
| from certidude import config, push, mailer, const | ||||
| from certidude import errors | ||||
| from crlbuilder import CertificateListBuilder, pem_armor_crl | ||||
| from csrbuilder import CSRBuilder, pem_armor_csr | ||||
| from datetime import datetime, timedelta | ||||
| from jinja2 import Template | ||||
| from random import SystemRandom | ||||
| from xattr import getxattr, listxattr, setxattr | ||||
|  | ||||
| random = SystemRandom() | ||||
|  | ||||
| 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]))?$" | ||||
|  | ||||
| # https://securityblog.redhat.com/2014/06/18/openssl-privilege-separation-analysis/ | ||||
| @@ -27,8 +29,14 @@ RE_HOSTNAME =  "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z | ||||
| # Cache CA certificate | ||||
|  | ||||
| with open(config.AUTHORITY_CERTIFICATE_PATH) as fh: | ||||
|     ca_buf = fh.read() | ||||
|     ca_cert = x509.load_pem_x509_certificate(ca_buf, default_backend()) | ||||
|     certificate_buf = fh.read() | ||||
|     header, _, certificate_der_bytes = pem.unarmor(certificate_buf) | ||||
|     certificate = x509.Certificate.load(certificate_der_bytes) | ||||
|     public_key = asymmetric.load_public_key(certificate["tbs_certificate"]["subject_public_key_info"]) | ||||
| with open(config.AUTHORITY_PRIVATE_KEY_PATH) as fh: | ||||
|     key_buf = fh.read() | ||||
|     header, _, key_der_bytes = pem.unarmor(key_buf) | ||||
|     private_key = asymmetric.load_private_key(key_der_bytes) | ||||
|  | ||||
| def get_request(common_name): | ||||
|     if not re.match(RE_HOSTNAME, common_name): | ||||
| @@ -37,7 +45,8 @@ def get_request(common_name): | ||||
|     try: | ||||
|         with open(path) as fh: | ||||
|             buf = fh.read() | ||||
|             return path, buf, x509.load_pem_x509_csr(buf, default_backend()) | ||||
|             header, _, der_bytes = pem.unarmor(buf) | ||||
|             return path, buf, CertificationRequest.load(der_bytes) | ||||
|     except EnvironmentError: | ||||
|         raise errors.RequestDoesNotExist("Certificate signing request file %s does not exist" % path) | ||||
|  | ||||
| @@ -47,13 +56,15 @@ def get_signed(common_name): | ||||
|     path = os.path.join(config.SIGNED_DIR, common_name + ".pem") | ||||
|     with open(path) as fh: | ||||
|         buf = fh.read() | ||||
|         return path, buf, x509.load_pem_x509_certificate(buf, default_backend()) | ||||
|         header, _, der_bytes = pem.unarmor(buf) | ||||
|         return path, buf, x509.Certificate.load(der_bytes) | ||||
|  | ||||
| def get_revoked(serial): | ||||
|     path = os.path.join(config.REVOKED_DIR, "%x.pem" % serial) | ||||
|     with open(path) as fh: | ||||
|         buf = fh.read() | ||||
|         return path, buf, x509.load_pem_x509_certificate(buf, default_backend()), \ | ||||
|         header, _, der_bytes = pem.unarmor(buf) | ||||
|         return path, buf, x509.Certificate.load(der_bytes), \ | ||||
|             datetime.utcfromtimestamp(os.stat(path).st_ctime) | ||||
|  | ||||
|  | ||||
| @@ -85,20 +96,19 @@ def store_request(buf, overwrite=False, address="", user=""): | ||||
|     if not buf: | ||||
|         raise ValueError("No signing request supplied") | ||||
|  | ||||
|     if isinstance(buf, unicode): | ||||
|         csr = x509.load_pem_x509_csr(buf.encode("ascii"), backend=default_backend()) | ||||
|     elif isinstance(buf, str): | ||||
|         csr = x509.load_der_x509_csr(buf, backend=default_backend()) | ||||
|         buf = csr.public_bytes(Encoding.PEM) | ||||
|     if pem.detect(buf): | ||||
|         header, _, der_bytes = pem.unarmor(buf) | ||||
|         csr = CertificationRequest.load(der_bytes) | ||||
|     else: | ||||
|         raise ValueError("Invalid type, expected str for PEM and bytes for DER") | ||||
|     common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME) | ||||
|     # TODO: validate common name again | ||||
|         csr = CertificationRequest.load(buf) | ||||
|         buf =  pem_armor_csr(csr) | ||||
|  | ||||
|     if not re.match(RE_HOSTNAME, common_name.value): | ||||
|     common_name = csr["certification_request_info"]["subject"].native["common_name"] | ||||
|  | ||||
|     if not re.match(RE_HOSTNAME, common_name): | ||||
|         raise ValueError("Invalid common name") | ||||
|  | ||||
|     request_path = os.path.join(config.REQUESTS_DIR, common_name.value + ".pem") | ||||
|     request_path = os.path.join(config.REQUESTS_DIR, common_name + ".pem") | ||||
|  | ||||
|  | ||||
|     # If there is cert, check if it's the same | ||||
| @@ -112,27 +122,13 @@ def store_request(buf, overwrite=False, address="", user=""): | ||||
|             fh.write(buf) | ||||
|         os.rename(request_path + ".part", request_path) | ||||
|  | ||||
|     attach_csr = buf, "application/x-pem-file", common_name.value + ".csr" | ||||
|     attach_csr = buf, "application/x-pem-file", common_name + ".csr" | ||||
|     mailer.send("request-stored.md", | ||||
|         attachments=(attach_csr,), | ||||
|         common_name=common_name.value) | ||||
|         common_name=common_name) | ||||
|     setxattr(request_path, "user.request.address", address) | ||||
|     setxattr(request_path, "user.request.user", user) | ||||
|     return request_path, csr, common_name.value | ||||
|  | ||||
|  | ||||
| def signer_exec(cmd, *bits): | ||||
|     sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | ||||
|     sock.connect(const.SIGNER_SOCKET_PATH) | ||||
|     sock.send(cmd.encode("ascii")) | ||||
|     sock.send(b"\n") | ||||
|     for bit in bits: | ||||
|         sock.send(bit.encode("ascii")) | ||||
|     sock.sendall(b"\n\n") | ||||
|     buf = sock.recv(8192) | ||||
|     if not buf: | ||||
|         raise Exception("Connection lost") | ||||
|     return buf | ||||
|     return request_path, csr, common_name | ||||
|  | ||||
|  | ||||
| def revoke(common_name): | ||||
| @@ -140,9 +136,9 @@ def revoke(common_name): | ||||
|     Revoke valid certificate | ||||
|     """ | ||||
|     signed_path, buf, cert = get_signed(common_name) | ||||
|     revoked_path = os.path.join(config.REVOKED_DIR, "%x.pem" % cert.serial) | ||||
|     revoked_path = os.path.join(config.REVOKED_DIR, "%x.pem" % cert.serial_number) | ||||
|     os.rename(signed_path, revoked_path) | ||||
|     os.unlink(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial)) | ||||
|     os.unlink(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial_number)) | ||||
|  | ||||
|     push.publish("certificate-revoked", common_name) | ||||
|  | ||||
| @@ -155,7 +151,7 @@ def revoke(common_name): | ||||
|     attach_cert = buf, "application/x-pem-file", common_name + ".crt" | ||||
|     mailer.send("certificate-revoked.md", | ||||
|         attachments=(attach_cert,), | ||||
|         serial_hex="%x" % cert.serial, | ||||
|         serial_hex="%x" % cert.serial_number, | ||||
|         common_name=common_name) | ||||
|     return revoked_path | ||||
|  | ||||
| @@ -186,12 +182,13 @@ def _list_certificates(directory): | ||||
|             path = os.path.join(directory, filename) | ||||
|             with open(path) as fh: | ||||
|                 buf = fh.read() | ||||
|                 cert = x509.load_pem_x509_certificate(buf, default_backend()) | ||||
|                 header, _, der_bytes = pem.unarmor(buf) | ||||
|                 cert = x509.Certificate.load(der_bytes) | ||||
|                 server = False | ||||
|                 extension = cert.extensions.get_extension_for_oid(ExtensionOID.EXTENDED_KEY_USAGE) | ||||
|                 for usage in extension.value: | ||||
|                     if usage == ExtendedKeyUsageOID.SERVER_AUTH: # TODO: IKE intermediate? | ||||
|                         server = True | ||||
|                 for extension in cert["tbs_certificate"]["extensions"]: | ||||
|                     if extension["extn_id"].native == u"extended_key_usage": | ||||
|                         if u"server_auth" in extension["extn_value"].native: | ||||
|                             server = True | ||||
|                 yield common_name, path, buf, cert, server | ||||
|  | ||||
| def list_signed(): | ||||
| @@ -203,10 +200,13 @@ def list_revoked(): | ||||
| def list_server_names(): | ||||
|     return [cn for cn, path, buf, cert, server in list_signed() if server] | ||||
|  | ||||
| def export_crl(): | ||||
|     sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | ||||
|     sock.connect(const.SIGNER_SOCKET_PATH) | ||||
|     sock.send(b"export-crl\n") | ||||
| def export_crl(pem=True): | ||||
|     builder = CertificateListBuilder( | ||||
|         config.AUTHORITY_CRL_URL, | ||||
|         certificate, | ||||
|         1 # TODO: monotonically increasing | ||||
|     ) | ||||
|  | ||||
|     for filename in os.listdir(config.REVOKED_DIR): | ||||
|         if not filename.endswith(".pem"): | ||||
|             continue | ||||
| @@ -215,9 +215,15 @@ def export_crl(): | ||||
|         revoked_path = os.path.join(config.REVOKED_DIR, filename) | ||||
|         # TODO: Skip expired certificates | ||||
|         s = os.stat(revoked_path) | ||||
|         sock.send(("%s:%d\n" % (serial_number, s.st_ctime)).encode("ascii")) | ||||
|     sock.sendall(b"\n") | ||||
|     return sock.recv(32*1024*1024) | ||||
|         builder.add_certificate( | ||||
|             int(filename[:-4], 16), | ||||
|             datetime.utcfromtimestamp(s.st_ctime), | ||||
|             u"key_compromise") | ||||
|  | ||||
|     certificate_list = builder.build(private_key) | ||||
|     if pem: | ||||
|         return pem_armor_crl(certificate_list) | ||||
|     return certificate_list.dump() | ||||
|  | ||||
|  | ||||
| def delete_request(common_name): | ||||
| @@ -236,88 +242,17 @@ def delete_request(common_name): | ||||
|         config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest(), | ||||
|         headers={"User-Agent": "Certidude API"}) | ||||
|  | ||||
| def generate_ovpn_bundle(common_name, owner=None): | ||||
|     # Construct private key | ||||
|     click.echo("Generating %d-bit RSA key for OpenVPN profile..." % const.KEY_SIZE) | ||||
|  | ||||
|     key = rsa.generate_private_key( | ||||
|         public_exponent=65537, | ||||
|         key_size=const.KEY_SIZE, | ||||
|         backend=default_backend() | ||||
|     ) | ||||
|  | ||||
|     key_buf = key.private_bytes( | ||||
|         encoding=serialization.Encoding.PEM, | ||||
|         format=serialization.PrivateFormat.TraditionalOpenSSL, | ||||
|         encryption_algorithm=serialization.NoEncryption() | ||||
|     ) | ||||
|  | ||||
|     csr = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([ | ||||
|         x509.NameAttribute(k, v) for k, v in ( | ||||
|             (NameOID.COMMON_NAME, common_name), | ||||
|         ) if v | ||||
|     ])).sign(key, hashes.SHA512(), default_backend()) | ||||
|  | ||||
|     buf = csr.public_bytes(serialization.Encoding.PEM) | ||||
|  | ||||
|     # Sign CSR | ||||
|     cert, cert_buf = _sign(csr, buf, overwrite=True) | ||||
|  | ||||
|     bundle = Template(open(config.OPENVPN_PROFILE_TEMPLATE).read()).render( | ||||
|         ca = ca_buf, key = key_buf, cert = cert_buf, crl=export_crl(), | ||||
|         servers = list_server_names()) | ||||
|     return bundle, cert | ||||
|  | ||||
| def generate_pkcs12_bundle(common_name, owner=None): | ||||
|     """ | ||||
|     Generate private key, sign certificate and return PKCS#12 bundle | ||||
|     """ | ||||
|  | ||||
|     # Construct private key | ||||
|     click.echo("Generating %d-bit RSA key for PKCS#12 bundle..." % const.KEY_SIZE) | ||||
|  | ||||
|     key = rsa.generate_private_key( | ||||
|         public_exponent=65537, | ||||
|         key_size=const.KEY_SIZE, | ||||
|         backend=default_backend() | ||||
|     ) | ||||
|  | ||||
|     csr = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([ | ||||
|         x509.NameAttribute(NameOID.COMMON_NAME, common_name) | ||||
|     ])).sign(key, hashes.SHA512(), default_backend()) | ||||
|  | ||||
|     buf = csr.public_bytes(serialization.Encoding.PEM) | ||||
|  | ||||
|     # Sign CSR | ||||
|     cert, cert_buf = _sign(csr, buf, 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()))) | ||||
|     p12.set_certificate( | ||||
|         crypto.load_certificate(crypto.FILETYPE_PEM, cert_buf)) | ||||
|     p12.set_ca_certificates([ | ||||
|         crypto.load_certificate(crypto.FILETYPE_PEM, ca_buf)]) | ||||
|     return p12.export("1234"), cert | ||||
|  | ||||
|  | ||||
| def sign(common_name, overwrite=False): | ||||
|     """ | ||||
|     Sign certificate signing request via signer process | ||||
|     Sign certificate signing request by it's common name | ||||
|     """ | ||||
|  | ||||
|     req_path = os.path.join(config.REQUESTS_DIR, common_name + ".pem") | ||||
|     with open(req_path) as fh: | ||||
|         csr_buf = fh.read() | ||||
|         csr = x509.load_pem_x509_csr(csr_buf, backend=default_backend()) | ||||
|     common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME) | ||||
|         header, _, der_bytes = pem.unarmor(csr_buf) | ||||
|         csr = CertificationRequest.load(der_bytes) | ||||
|  | ||||
|  | ||||
|     # Sign with function below | ||||
|     cert, buf = _sign(csr, csr_buf, overwrite) | ||||
| @@ -326,15 +261,17 @@ def sign(common_name, overwrite=False): | ||||
|     return cert, buf | ||||
|  | ||||
| def _sign(csr, buf, overwrite=False): | ||||
|     assert buf.startswith("-----BEGIN CERTIFICATE REQUEST-----\n") | ||||
|     assert isinstance(csr, x509.CertificateSigningRequest) | ||||
|     # TODO: CRLDistributionPoints, OCSP URL, Certificate URL | ||||
|  | ||||
|     common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME) | ||||
|     cert_path = os.path.join(config.SIGNED_DIR, "%s.pem" % common_name.value) | ||||
|     assert buf.startswith("-----BEGIN CERTIFICATE REQUEST-----\n") | ||||
|     assert isinstance(csr, CertificationRequest) | ||||
|     csr_pubkey = asymmetric.load_public_key(csr["certification_request_info"]["subject_pk_info"]) | ||||
|     common_name = csr["certification_request_info"]["subject"].native["common_name"] | ||||
|     cert_path = os.path.join(config.SIGNED_DIR, "%s.pem" % common_name) | ||||
|     renew = False | ||||
|  | ||||
|     attachments = [ | ||||
|         (buf, "application/x-pem-file", common_name.value + ".csr"), | ||||
|         (buf, "application/x-pem-file", common_name + ".csr"), | ||||
|     ] | ||||
|  | ||||
|     revoked_path = None | ||||
| @@ -344,13 +281,18 @@ def _sign(csr, buf, overwrite=False): | ||||
|     if os.path.exists(cert_path): | ||||
|         with open(cert_path) as fh: | ||||
|             prev_buf = fh.read() | ||||
|             prev = x509.load_pem_x509_certificate(prev_buf, default_backend()) | ||||
|             header, _, der_bytes = pem.unarmor(prev_buf) | ||||
|             prev = x509.Certificate.load(der_bytes) | ||||
|  | ||||
|             # TODO: assert validity here again? | ||||
|             renew = prev.public_key().public_numbers() == csr.public_key().public_numbers() | ||||
|             renew = \ | ||||
|                 asymmetric.load_public_key(prev["tbs_certificate"]["subject_public_key_info"]) == \ | ||||
|                 csr_pubkey | ||||
|                 # BUGBUG: is this enough? | ||||
|  | ||||
|         if overwrite: | ||||
|             # TODO: is this the best approach? | ||||
|             prev_serial_hex = "%x" % prev.serial | ||||
|             prev_serial_hex = "%x" % prev.serial_number | ||||
|             revoked_path = os.path.join(config.REVOKED_DIR, "%s.pem" % prev_serial_hex) | ||||
|             os.rename(cert_path, revoked_path) | ||||
|             attachments += [(prev_buf, "application/x-pem-file", "deprecated.crt" if renew else "overwritten.crt")] | ||||
| @@ -359,18 +301,40 @@ def _sign(csr, buf, overwrite=False): | ||||
|             raise EnvironmentError("Will not overwrite existing certificate") | ||||
|  | ||||
|     # Sign via signer process | ||||
|     cert_buf = signer_exec("sign-request", buf) | ||||
|     cert = x509.load_pem_x509_certificate(cert_buf, default_backend()) | ||||
|     builder = CertificateBuilder({u'common_name': common_name }, csr_pubkey) | ||||
|     builder.serial_number = random.randint( | ||||
|         0x1000000000000000000000000000000000000000, | ||||
|         0xffffffffffffffffffffffffffffffffffffffff) | ||||
|  | ||||
|     now = datetime.utcnow() | ||||
|     builder.begin_date = now - timedelta(minutes=5) | ||||
|     builder.end_date = now + timedelta(days=config.SERVER_CERTIFICATE_LIFETIME | ||||
|         if server_flags(common_name) | ||||
|         else config.CLIENT_CERTIFICATE_LIFETIME) | ||||
|     builder.issuer = certificate | ||||
|     builder.ca = False | ||||
|     builder.key_usage = set([u"digital_signature", u"key_encipherment"]) | ||||
|  | ||||
|     # OpenVPN uses CN while StrongSwan uses SAN | ||||
|     if server_flags(common_name): | ||||
|         builder.subject_alt_domains = [common_name] | ||||
|         builder.extended_key_usage = set([u"server_auth", u"1.3.6.1.5.5.8.2.2", u"client_auth"]) | ||||
|     else: | ||||
|         builder.extended_key_usage = set([u"client_auth"]) | ||||
|  | ||||
|     end_entity_cert = builder.build(private_key) | ||||
|     end_entity_cert_buf = asymmetric.dump_certificate(end_entity_cert) | ||||
|     with open(cert_path + ".part", "wb") as fh: | ||||
|         fh.write(cert_buf) | ||||
|         fh.write(end_entity_cert_buf) | ||||
|  | ||||
|     os.rename(cert_path + ".part", cert_path) | ||||
|     attachments.append((cert_buf, "application/x-pem-file", common_name.value + ".crt")) | ||||
|     cert_serial_hex = "%x" % cert.serial | ||||
|     attachments.append((end_entity_cert_buf, "application/x-pem-file", common_name + ".crt")) | ||||
|     cert_serial_hex = "%x" % end_entity_cert.serial_number | ||||
|  | ||||
|     # Create symlink | ||||
|     os.symlink( | ||||
|         "../%s.pem" % common_name.value, | ||||
|         os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial)) | ||||
|     link_name = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % end_entity_cert.serial_number) | ||||
|     assert not os.path.exists(link_name), "Certificate with same serial number already exists: %s" % link_name | ||||
|     os.symlink("../%s.pem" % common_name, link_name) | ||||
|  | ||||
|     # Copy filesystem attributes to newly signed certificate | ||||
|     if revoked_path: | ||||
| @@ -387,8 +351,8 @@ def _sign(csr, buf, overwrite=False): | ||||
|  | ||||
|     url = config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest() | ||||
|     click.echo("Publishing certificate at %s ..." % url) | ||||
|     requests.post(url, data=cert_buf, | ||||
|     requests.post(url, data=end_entity_cert_buf, | ||||
|         headers={"User-Agent": "Certidude API", "Content-Type": "application/x-x509-user-cert"}) | ||||
|  | ||||
|     push.publish("request-signed", common_name.value) | ||||
|     return cert, cert_buf | ||||
|     push.publish("request-signed", common_name) | ||||
|     return end_entity_cert, end_entity_cert_buf | ||||
|   | ||||
							
								
								
									
										128
									
								
								certidude/cli.py
									
									
									
									
									
								
							
							
						
						
									
										128
									
								
								certidude/cli.py
									
									
									
									
									
								
							| @@ -12,6 +12,7 @@ import socket | ||||
| import string | ||||
| import subprocess | ||||
| import sys | ||||
| from asn1crypto.util import timezone | ||||
| from base64 import b64encode | ||||
| from configparser import ConfigParser, NoOptionError, NoSectionError | ||||
| from certidude.common import ip_address, ip_network, apt, rpm, pip, drop_privileges, selinux_fixup | ||||
| @@ -26,9 +27,7 @@ logger = logging.getLogger(__name__) | ||||
| # keyUsage, extendedKeyUsage - https://www.openssl.org/docs/apps/x509v3_client_config.html | ||||
| # strongSwan key paths - https://wiki.strongswan.org/projects/1/wiki/SimpleCA | ||||
|  | ||||
| # Parse command-line argument defaults from environment | ||||
|  | ||||
| NOW = datetime.utcnow().replace(tzinfo=None) | ||||
| NOW = datetime.utcnow() | ||||
|  | ||||
| def fqdn_required(func): | ||||
|     def wrapped(**args): | ||||
| @@ -321,8 +320,8 @@ def certidude_request(fork, renew, no_wait, kerberos): | ||||
|             with open(certificate_path, "rb") as ch, open(request_path, "rb") as rh, open(key_path, "rb") as kh: | ||||
|                 cert_buf = ch.read() | ||||
|                 cert = asymmetric.load_certificate(cert_buf) | ||||
|                 expires = cert.asn1["tbs_certificate"]["validity"]["not_after"].native | ||||
|                 if renewal_overlap and datetime.now() > expires - timedelta(days=renewal_overlap): | ||||
|                 expires = cert.asn1["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None) | ||||
|                 if renewal_overlap and NOW > expires - timedelta(days=renewal_overlap): | ||||
|                     click.echo("Certificate will expire %s, will attempt to renew" % expires) | ||||
|                     renew = True | ||||
|                 headers["X-Renewal-Signature"] = b64encode( | ||||
| @@ -931,15 +930,13 @@ def certidude_setup_openvpn_networkmanager(authority, remote, common_name, **pat | ||||
| @fqdn_required | ||||
| def certidude_setup_authority(username, kerberos_keytab, nginx_config, country, state, locality, organization, organizational_unit, common_name, directory, authority_lifetime, push_server, outbox, server_flags): | ||||
|     # Install only rarely changing stuff from OS package management | ||||
|     apt("python-setproctitle cython python-dev libkrb5-dev libffi-dev libssl-dev") | ||||
|     apt("python-mimeparse python-markdown python-xattr python-jinja2 python-cffi") | ||||
|     apt("python-ldap software-properties-common libsasl2-modules-gssapi-mit") | ||||
|     pip("gssapi falcon humanize ipaddress simplepam humanize requests pyopenssl") | ||||
|     apt("cython python-dev python-mimeparse python-markdown python-xattr python-jinja2 python-cffi python-ldap software-properties-common libsasl2-modules-gssapi-mit") | ||||
|     pip("gssapi falcon humanize ipaddress simplepam humanize requests") | ||||
|     click.echo("Software dependencies installed") | ||||
|  | ||||
|     os.system("add-apt-repository -y ppa:nginx/stable") | ||||
|     os.system("apt-get update") | ||||
|     if not os.path.exists("/usr/lib/nginx/modules/ngx_nchan_module.so"): | ||||
|         os.system("add-apt-repository -y ppa:nginx/stable") | ||||
|         os.system("apt-get update") | ||||
|         os.system("apt-get install -y libnginx-mod-nchan") | ||||
|     if not os.path.exists("/usr/sbin/nginx"): | ||||
|         os.system("apt-get install -y nginx") | ||||
| @@ -1091,13 +1088,13 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country, | ||||
|         builder.serial_number = random.randint( | ||||
|             0x100000000000000000000000000000000000000, | ||||
|             0xfffffffffffffffffffffffffffffffffffffff) | ||||
|         now = datetime.utcnow() | ||||
|         builder.begin_date = now - timedelta(minutes=5) | ||||
|         builder.end_date = now + timedelta(days=authority_lifetime) | ||||
|  | ||||
|         builder.begin_date = NOW - timedelta(minutes=5) | ||||
|         builder.end_date = NOW + timedelta(days=authority_lifetime) | ||||
|  | ||||
|         if server_flags: | ||||
|             builder.key_usage(set(['digital_signature', 'key_encipherment', 'key_cert_sign', 'crl_sign'])) | ||||
|             builder.extended_key_usage(['server_auth', "1.3.6.1.5.5.8.2.2"]) | ||||
|             builder.key_usage = set(['digital_signature', 'key_encipherment', 'key_cert_sign', 'crl_sign']) | ||||
|             builder.extended_key_usage = set(['server_auth', "1.3.6.1.5.5.8.2.2"]) | ||||
|  | ||||
|         certificate = builder.build(private_key) | ||||
|  | ||||
| @@ -1162,9 +1159,6 @@ def certidude_list(verbose, show_key_type, show_extensions, show_path, show_sign | ||||
|             click.echo("sha1sum: %s" % hashlib.sha1(buf).hexdigest()) | ||||
|             click.echo("sha256sum: %s" % hashlib.sha256(buf).hexdigest()) | ||||
|         click.echo() | ||||
|         for ext in cert.extensions: | ||||
|             print " -", ext.value | ||||
|         click.echo() | ||||
|  | ||||
|     if not hide_requests: | ||||
|         for common_name, path, buf, csr, server in authority.list_requests(): | ||||
| @@ -1172,6 +1166,7 @@ def certidude_list(verbose, show_key_type, show_extensions, show_path, show_sign | ||||
|             if not verbose: | ||||
|                 click.echo("s " + path) | ||||
|                 continue | ||||
|             click.echo() | ||||
|             click.echo(click.style(common_name, fg="blue")) | ||||
|             click.echo("=" * len(common_name)) | ||||
|             click.echo("State: ? " + click.style("submitted", fg="yellow") + " " + naturaltime(created) + click.style(", %s" %created,  fg="white")) | ||||
| @@ -1181,35 +1176,39 @@ def certidude_list(verbose, show_key_type, show_extensions, show_path, show_sign | ||||
|  | ||||
|     if show_signed: | ||||
|         for common_name, path, buf, cert, server in authority.list_signed(): | ||||
|             signed = cert["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None) | ||||
|             expires = cert["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None) | ||||
|             if not verbose: | ||||
|                 if cert.not_valid_before < NOW and cert.not_valid_after > NOW: | ||||
|                 if signed < NOW and NOW < expires: | ||||
|                     click.echo("v " + path) | ||||
|                 elif NOW > cert.not_valid_after: | ||||
|                 elif expires < NOW: | ||||
|                     click.echo("e " + path) | ||||
|                 else: | ||||
|                     click.echo("y " + path) | ||||
|                 continue | ||||
|  | ||||
|             click.echo(click.style(common_name, fg="blue") + " " + click.style("%x" % cert.serial, fg="white")) | ||||
|             click.echo() | ||||
|             click.echo(click.style(common_name, fg="blue") + " " + click.style("%x" % cert.serial_number, fg="white")) | ||||
|             click.echo("="*(len(common_name)+60)) | ||||
|             expires = 0 # TODO | ||||
|             if cert.not_valid_before < NOW and cert.not_valid_after > NOW: | ||||
|                 click.echo("Status: " + click.style("valid", fg="green") + " until " + naturaltime(cert.not_valid_after) + click.style(", %s" % cert.not_valid_after,  fg="white")) | ||||
|             elif NOW > cert.not_valid_after: | ||||
|                 click.echo("Status: " + click.style("expired", fg="red") + " " + naturaltime(expires) + click.style(", %s" %expires,  fg="white")) | ||||
|  | ||||
|             if signed < NOW and NOW < expires: | ||||
|                 click.echo("Status: " + click.style("valid", fg="green") + " until " + naturaltime(expires) + click.style(", %s" % expires,  fg="white")) | ||||
|             elif NOW > expires: | ||||
|                 click.echo("Status: " + click.style("expired", fg="red") + " " + naturaltime(expires) + click.style(", %s" % expires,  fg="white")) | ||||
|             else: | ||||
|                 click.echo("Status: " + click.style("not valid yet", fg="red") + click.style(", %s" %expires,  fg="white")) | ||||
|                 click.echo("Status: " + click.style("not valid yet", fg="red") + click.style(", %s" % expires,  fg="white")) | ||||
|             click.echo() | ||||
|             click.echo("openssl x509 -in %s -text -noout" % path) | ||||
|             dump_common(common_name, path, cert) | ||||
|             for ext in cert["tbs_certificate"]["extensions"]: | ||||
|                 print " - %s: %s" % (ext["extn_id"].native, repr(ext["extn_value"].native)) | ||||
|  | ||||
|     if show_revoked: | ||||
|         for common_name, path, buf, cert, server in authority.list_revoked(): | ||||
|             if not verbose: | ||||
|                 click.echo("r " + path) | ||||
|                 continue | ||||
|  | ||||
|             click.echo(click.style(common_name, fg="blue") + " " + click.style("%x" % cert.serial, fg="white")) | ||||
|             click.echo() | ||||
|             click.echo(click.style(common_name, fg="blue") + " " + click.style("%x" % cert.serial_number, fg="white")) | ||||
|             click.echo("="*(len(common_name)+60)) | ||||
|  | ||||
|             _, _, _, _, _, _, _, _, mtime, _ = os.stat(path) | ||||
| @@ -1217,24 +1216,24 @@ def certidude_list(verbose, show_key_type, show_extensions, show_path, show_sign | ||||
|             click.echo("Status: " + click.style("revoked", fg="red") + " %s%s" % (naturaltime(NOW-changed), click.style(", %s" % changed, fg="white"))) | ||||
|             click.echo("openssl x509 -in %s -text -noout" % path) | ||||
|             dump_common(common_name, path, cert) | ||||
|  | ||||
|     click.echo() | ||||
|             for ext in cert["tbs_certificate"]["extensions"]: | ||||
|                 print " - %s: %s" % (ext["extn_id"].native, repr(ext["extn_value"].native)) | ||||
|  | ||||
|  | ||||
| @click.command("sign", help="Sign certificate") | ||||
| @click.argument("common_name") | ||||
| @click.option("--overwrite", "-o", default=False, is_flag=True, help="Revoke valid certificate with same CN") | ||||
| def certidude_sign(common_name, overwrite): | ||||
|     drop_privileges() | ||||
|     from certidude import authority | ||||
|     drop_privileges() | ||||
|     cert = authority.sign(common_name, overwrite) | ||||
|  | ||||
|  | ||||
| @click.command("revoke", help="Revoke certificate") | ||||
| @click.argument("common_name") | ||||
| def certidude_revoke(common_name): | ||||
|     drop_privileges() | ||||
|     from certidude import authority | ||||
|     drop_privileges() | ||||
|     authority.revoke(common_name) | ||||
|  | ||||
|  | ||||
| @@ -1242,10 +1241,10 @@ def certidude_revoke(common_name): | ||||
| def certidude_cron(): | ||||
|     import itertools | ||||
|     from certidude import authority, config | ||||
|     now = datetime.now() | ||||
|     for cn, path, buf, cert, server in itertools.chain(authority.list_signed(), authority.list_revoked()): | ||||
|         if cert.not_valid_after < now: | ||||
|             expired_path = os.path.join(config.EXPIRED_DIR, "%x.pem" % cert.serial) | ||||
|         expires = cert["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None) | ||||
|         if expires < NOW: | ||||
|             expired_path = os.path.join(config.EXPIRED_DIR, "%x.pem" % cert.serial_number) | ||||
|             assert not os.path.exists(expired_path) | ||||
|             os.rename(path, expired_path) | ||||
|             click.echo("Moved %s to %s" % (path, expired_path)) | ||||
| @@ -1258,7 +1257,6 @@ def certidude_cron(): | ||||
| def certidude_serve(port, listen, fork): | ||||
|     import pwd | ||||
|     from setproctitle import setproctitle | ||||
|     from certidude.signer import SignServer | ||||
|     from certidude import authority, const, push | ||||
|  | ||||
|     if port == 80: | ||||
| @@ -1272,7 +1270,7 @@ def certidude_serve(port, listen, fork): | ||||
|  | ||||
|     # Rebuild reverse mapping | ||||
|     for cn, path, buf, cert, server in authority.list_signed(): | ||||
|         by_serial = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial) | ||||
|         by_serial = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial_number) | ||||
|         if not os.path.exists(by_serial): | ||||
|             click.echo("Linking %s to ../%s.pem" % (by_serial, cn)) | ||||
|             os.symlink("../%s.pem" % cn, by_serial) | ||||
| @@ -1291,55 +1289,6 @@ def certidude_serve(port, listen, fork): | ||||
|     rh.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")) | ||||
|     log_handlers.append(rh) | ||||
|  | ||||
|  | ||||
|     """ | ||||
|     Spawn signer process | ||||
|     """ | ||||
|  | ||||
|     if os.path.exists(const.SIGNER_SOCKET_PATH): | ||||
|         os.unlink(const.SIGNER_SOCKET_PATH) | ||||
|  | ||||
|     signer_pid = os.fork() | ||||
|     if not signer_pid: | ||||
|         click.echo("Signer process spawned with PID %d at %s" % (os.getpid(), const.SIGNER_SOCKET_PATH)) | ||||
|         setproctitle("[signer]") | ||||
|  | ||||
|         with open(const.SIGNER_PID_PATH, "w") as fh: | ||||
|             fh.write("%d\n" % os.getpid()) | ||||
|  | ||||
|         logging.basicConfig( | ||||
|             filename=const.SIGNER_LOG_PATH, | ||||
|             level=logging.INFO) | ||||
|  | ||||
|         os.umask(0o007) | ||||
|         server = SignServer() | ||||
|  | ||||
|         # Drop privileges | ||||
|         _, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude") | ||||
|         os.chown(const.SIGNER_SOCKET_PATH, uid, gid) | ||||
|         os.chmod(const.SIGNER_SOCKET_PATH, 0770) | ||||
|  | ||||
|         click.echo("Dropping privileges of signer") | ||||
|         _, _, uid, gid, gecos, root, shell = pwd.getpwnam("nobody") | ||||
|         os.setgroups([]) | ||||
|         os.setgid(gid) | ||||
|         os.setuid(uid) | ||||
|  | ||||
|         try: | ||||
|             asyncore.loop() | ||||
|         except asyncore.ExitNow: | ||||
|             pass | ||||
|         click.echo("Signer was shut down") | ||||
|         return | ||||
|     click.echo("Waiting for signer to start up") | ||||
|     time_left = 2.0 | ||||
|     delay = 0.1 | ||||
|     while not os.path.exists(const.SIGNER_SOCKET_PATH) and time_left > 0: | ||||
|         sleep(delay) | ||||
|         time_left -= delay | ||||
|     assert authority.signer_exec("ping") == "pong" | ||||
|     click.echo("Signer alive") | ||||
|  | ||||
|     click.echo("Users subnets: %s" % | ||||
|         ", ".join([str(j) for j in config.USER_SUBNETS])) | ||||
|     click.echo("Administrative subnets: %s" % | ||||
| @@ -1392,7 +1341,6 @@ def certidude_serve(port, listen, fork): | ||||
|         def cleanup_handler(*args): | ||||
|             push.publish("server-stopped") | ||||
|             logger.debug(u"Shutting down Certidude") | ||||
|             assert authority.signer_exec("exit") == "ok" | ||||
|             sys.exit(0) # TODO: use another code, needs test refactor | ||||
|  | ||||
|         import signal | ||||
|   | ||||
| @@ -12,9 +12,6 @@ CLIENT_CONFIG_PATH = os.path.join(CONFIG_DIR, "client.conf") | ||||
| SERVICES_CONFIG_PATH = os.path.join(CONFIG_DIR, "services.conf") | ||||
| SERVER_PID_PATH = os.path.join(RUN_DIR, "server.pid") | ||||
| SERVER_LOG_PATH = "/var/log/certidude-server.log" | ||||
| SIGNER_SOCKET_PATH = "/run/certidude/signer.sock" | ||||
| SIGNER_PID_PATH = os.path.join(RUN_DIR, "signer.pid") | ||||
| SIGNER_LOG_PATH = "/var/log/certidude-signer.log" | ||||
| STORAGE_PATH = "/var/lib/certidude/" | ||||
|  | ||||
| try: | ||||
|   | ||||
| @@ -1,235 +0,0 @@ | ||||
|  | ||||
|  | ||||
| import random | ||||
| import socket | ||||
| import os | ||||
| import asyncore | ||||
| import asynchat | ||||
| from base64 import b64decode, b64encode | ||||
| from certidude import const, config | ||||
| from cryptography import x509 | ||||
| from cryptography.hazmat.backends import default_backend | ||||
| from cryptography.hazmat.primitives import hashes, padding, serialization | ||||
| from cryptography.hazmat.primitives.serialization import Encoding | ||||
| from cryptography.hazmat.primitives.asymmetric import padding | ||||
| from datetime import datetime, timedelta | ||||
| from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID, AuthorityInformationAccessOID | ||||
| import random | ||||
|  | ||||
| class SignHandler(asynchat.async_chat): | ||||
|     def __init__(self, sock, server): | ||||
|         asynchat.async_chat.__init__(self, sock=sock) | ||||
|         self.buffer = [] | ||||
|         self.set_terminator(b"\n\n") | ||||
|         self.server = server | ||||
|  | ||||
|     def parse_command(self, cmd, body=""): | ||||
|         now = datetime.utcnow() | ||||
|         if cmd == "export-crl": | ||||
|             """ | ||||
|             Generate CRL object based on certificate serial number and revocation timestamp | ||||
|             """ | ||||
|  | ||||
|             builder = x509.CertificateRevocationListBuilder( | ||||
|                 ).last_update( | ||||
|                     now - timedelta(minutes=5) | ||||
|                 ).next_update( | ||||
|                     now + timedelta(seconds=config.REVOCATION_LIST_LIFETIME) | ||||
|                 ).issuer_name(self.server.certificate.issuer | ||||
|                 ).add_extension( | ||||
|                     x509.AuthorityKeyIdentifier.from_issuer_public_key( | ||||
|                         self.server.certificate.public_key()), False) | ||||
|  | ||||
|             if body: | ||||
|                 for line in body.split("\n"): | ||||
|                     serial_number, timestamp = line.split(":") | ||||
|                     revocation = x509.RevokedCertificateBuilder( | ||||
|                         ).serial_number(int(serial_number, 16) | ||||
|                         ).revocation_date(datetime.utcfromtimestamp(int(timestamp)) | ||||
|                         ).add_extension(x509.CRLReason(x509.ReasonFlags.key_compromise), False | ||||
|                         ).build(default_backend()) | ||||
|                     builder = builder.add_revoked_certificate(revocation) | ||||
|  | ||||
|             crl = builder.sign( | ||||
|                 self.server.private_key, | ||||
|                 hashes.SHA512(), | ||||
|                 default_backend()) | ||||
|  | ||||
|             self.send(crl.public_bytes(Encoding.PEM)) | ||||
|  | ||||
|         elif cmd == "ping": | ||||
|             self.send("pong") | ||||
|             self.close() | ||||
|  | ||||
|         elif cmd == "exit": | ||||
|             self.send("ok") | ||||
|             self.close() | ||||
|             raise asyncore.ExitNow() | ||||
|  | ||||
|         elif cmd == "sign-pkcs7": | ||||
|             signer = self.server.private_key.signer( | ||||
|                 padding.PKCS1v15(), | ||||
|                 hashes.SHA1() | ||||
|             ) | ||||
|             signer.update(b64decode(body)) | ||||
|             self.send(b64encode(signer.finalize())) | ||||
|  | ||||
|         elif cmd == "decrypt-pkcs7": | ||||
|             self.send(b64encode(self.server.private_key.decrypt(b64decode(body), padding.PKCS1v15()))) | ||||
|             self.close() | ||||
|  | ||||
|         elif cmd == "sign-request": | ||||
|             # Only common name and public key are used from request | ||||
|             request = x509.load_pem_x509_csr(body, default_backend()) | ||||
|             common_name, = request.subject.get_attributes_for_oid(NameOID.COMMON_NAME) | ||||
|  | ||||
|             # 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) | ||||
|  | ||||
|             aia = [ | ||||
|                 x509.AccessDescription( | ||||
|                     AuthorityInformationAccessOID.CA_ISSUERS, | ||||
|                     x509.UniformResourceIdentifier(config.AUTHORITY_CERTIFICATE_URL)) | ||||
|             ] | ||||
|  | ||||
|             if config.AUTHORITY_OCSP_URL: | ||||
|                 aia.append( | ||||
|                     x509.AccessDescription( | ||||
|                         AuthorityInformationAccessOID.OCSP, | ||||
|                         x509.UniformResourceIdentifier(config.AUTHORITY_OCSP_URL))) | ||||
|  | ||||
|             builder = x509.CertificateBuilder( | ||||
|                 ).subject_name( | ||||
|                     x509.Name([common_name]) | ||||
|                 ).serial_number(random.randint( | ||||
|                     0x1000000000000000000000000000000000000000, | ||||
|                     0x7fffffffffffffffffffffffffffffffffffffff) | ||||
|                 ).issuer_name( | ||||
|                     self.server.certificate.issuer | ||||
|                 ).public_key( | ||||
|                     request.public_key() | ||||
|                 ).not_valid_before( | ||||
|                     now - timedelta(minutes=5) | ||||
|                 ).not_valid_after( | ||||
|                     now + timedelta(days= | ||||
|                         config.SERVER_CERTIFICATE_LIFETIME | ||||
|                         if server_flags | ||||
|                         else config.CLIENT_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( | ||||
|                         extended_key_usage_flags), | ||||
|                     critical=True, | ||||
|                 ).add_extension( | ||||
|                     x509.SubjectKeyIdentifier.from_public_key( | ||||
|                         request.public_key()), | ||||
|                     critical=False | ||||
|                 ).add_extension( | ||||
|                     x509.AuthorityInformationAccess(aia), | ||||
|                     critical=False | ||||
|                 ).add_extension( | ||||
|                     x509.AuthorityKeyIdentifier.from_issuer_public_key( | ||||
|                         self.server.certificate.public_key()), | ||||
|                     critical=False | ||||
|                 ) | ||||
|  | ||||
|             if config.AUTHORITY_CRL_URL: | ||||
|                 builder = builder.add_extension( | ||||
|                     x509.CRLDistributionPoints([ | ||||
|                         x509.DistributionPoint( | ||||
|                             full_name=[ | ||||
|                                 x509.UniformResourceIdentifier( | ||||
|                                     config.AUTHORITY_CRL_URL)], | ||||
|                             relative_name=None, | ||||
|                             crl_issuer=None, | ||||
|                             reasons=None) | ||||
|                     ]), | ||||
|                     critical=False | ||||
|                 ) | ||||
|  | ||||
|             # OpenVPN uses CN while StrongSwan uses SAN | ||||
|             if server_flags: | ||||
|                 builder = builder.add_extension( | ||||
|                     x509.SubjectAlternativeName( | ||||
|                         [x509.DNSName(common_name.value)] | ||||
|                     ), | ||||
|                     critical=False | ||||
|                 ) | ||||
|  | ||||
|             cert = builder.sign(self.server.private_key, hashes.SHA512(), default_backend()) | ||||
|  | ||||
|             self.send(cert.public_bytes(serialization.Encoding.PEM)) | ||||
|         else: | ||||
|             raise NotImplementedError("Unknown command: %s" % cmd) | ||||
|  | ||||
|         self.close_when_done() | ||||
|  | ||||
|     def found_terminator(self): | ||||
|         args = (b"".join(self.buffer)).split("\n", 1) | ||||
|         self.parse_command(*args) | ||||
|         self.buffer = [] | ||||
|  | ||||
|     def collect_incoming_data(self, data): | ||||
|         self.buffer.append(data) | ||||
|  | ||||
| import signal | ||||
| import click | ||||
|  | ||||
| class SignServer(asyncore.dispatcher): | ||||
|     def __init__(self): | ||||
|         asyncore.dispatcher.__init__(self) | ||||
|  | ||||
|         if os.path.exists(const.SIGNER_SOCKET_PATH): | ||||
|             os.unlink(const.SIGNER_SOCKET_PATH) | ||||
|  | ||||
|         self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM) | ||||
|         self.bind(const.SIGNER_SOCKET_PATH) | ||||
|         self.listen(5) | ||||
|  | ||||
|         # Load CA private key and certificate | ||||
|         click.echo("Signer reading private key from %s" % config.AUTHORITY_PRIVATE_KEY_PATH) | ||||
|         self.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()) | ||||
|         click.echo("Signer reading certificate from %s" % config.AUTHORITY_CERTIFICATE_PATH) | ||||
|         self.certificate = x509.load_pem_x509_certificate( | ||||
|             open(config.AUTHORITY_CERTIFICATE_PATH).read(), | ||||
|             backend=default_backend()) | ||||
|  | ||||
|  | ||||
|     def handle_accept(self): | ||||
|         pair = self.accept() | ||||
|         if pair is not None: | ||||
|             sock, addr = pair | ||||
|             handler = SignHandler(sock, self) | ||||
|  | ||||
| @@ -1,7 +1,7 @@ | ||||
| <li id="request-{{ request.common_name | replace('@', '--') | replace('.', '-') }}" class="filterable"> | ||||
|  | ||||
| <a class="button icon download" href="/api/request/{{request.common_name}}/">Fetch</a> | ||||
| <button class="icon sign" onClick="javascript:$(this).addClass('busy');$.ajax({url:'/api/request/{{request.common_name}}/?sha256sum={{ request.sha256sum }}',type:'patch'});">Sign</button> | ||||
| <button class="icon sign" onClick="javascript:$(this).addClass('busy');$.ajax({url:'/api/request/{{request.common_name}}/?sha256sum={{ request.sha256sum }}',type:'post'});">Sign</button> | ||||
| <button class="icon revoke" onClick="javascript:$(this).addClass('busy');$.ajax({url:'/api/request/{{request.common_name}}/?sha256sum={{ request.sha256sum }}',type:'delete'});">Delete</button> | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| Renewed {{ common_name.value }} ({{ cert_serial_hex }}) | ||||
| Renewed {{ common_name }} ({{ cert_serial_hex }}) | ||||
|  | ||||
| This is simply to notify that certificate for {{ common_name.value }} | ||||
| This is simply to notify that certificate for {{ common_name }} | ||||
| was renewed and the serial number of the new certificate is {{ cert_serial_hex }}. | ||||
|  | ||||
| The new certificate is valid from {{ cert.not_valid_before }} until | ||||
| {{ cert.not_valid_after }}. | ||||
| The new certificate is valid from {{ builder.begin_date }} until | ||||
| {{ builder.end_date }}. | ||||
|  | ||||
| Services making use of those certificates should continue working as expected. | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| Signed {{ common_name.value }} ({{ cert_serial_hex }}) | ||||
| Signed {{ common_name }} ({{ cert_serial_hex }}) | ||||
|  | ||||
| This is simply to notify that certificate {{ common_name.value }} | ||||
| This is simply to notify that certificate {{ common_name }} | ||||
| with serial number {{ cert_serial_hex }} | ||||
| was signed{% if signer %} by {{ signer }}{% endif %}. | ||||
|  | ||||
| The certificate is valid from {{ cert.not_valid_before }} until | ||||
| {{ cert.not_valid_after }}. | ||||
| The certificate is valid from {{ builder.begin_date }} until | ||||
| {{ builder.end_date }}. | ||||
|  | ||||
| {% if overwritten %} | ||||
| By doing so existing certificate with the same common name | ||||
|   | ||||
| @@ -1,69 +1,136 @@ | ||||
| # To set up SSL certificates using Let's Encrypt run: | ||||
| # | ||||
| #   apt install letsencrypt | ||||
| #   certbot certonly -d {{common_name}} --webroot /var/www/html/ | ||||
|  | ||||
| # | ||||
| # Also uncomment URL rewriting and SSL configuration below | ||||
|  | ||||
| # Basic DoS prevention measures | ||||
| limit_conn addr 10; | ||||
| client_body_timeout 5s; | ||||
| client_header_timeout 5s; | ||||
| limit_req_zone $binary_remote_addr  zone=api:10m rate=30r/m; | ||||
| limit_conn_zone $binary_remote_addr zone=addr:10m; | ||||
|  | ||||
| # Backend configuration | ||||
| proxy_set_header Host $host; | ||||
| proxy_set_header X-Real-IP $remote_addr; | ||||
| proxy_set_header X-SSL-CERT $ssl_client_cert; | ||||
| proxy_connect_timeout 600; | ||||
| proxy_send_timeout 600; | ||||
| proxy_read_timeout 600; | ||||
| proxy_set_header Host $host; | ||||
| send_timeout 600; | ||||
|  | ||||
| # Don't buffer any messages | ||||
| nchan_message_buffer_length 0; | ||||
|  | ||||
| # To use CA-s own certificate for HTTPS | ||||
| ssl_certificate /var/lib/certidude/{{common_name}}/ca_crt.pem; | ||||
| ssl_certificate_key /var/lib/certidude/{{common_name}}/ca_key.pem; | ||||
|  | ||||
| # To use Let's Encrypt certificates | ||||
| #ssl_certificate /etc/letsencrypt/live/{{common_name}}/fullchain.pem; | ||||
| #ssl_certificate_key /etc/letsencrypt/live/{{common_name}}/privkey.pem; | ||||
|  | ||||
| # Also run the following to set up Let's Encrypt certificates: | ||||
| # | ||||
| #   apt install letsencrypt | ||||
| #   certbot certonly -d {{common_name}} --webroot /var/www/html/ | ||||
|  | ||||
| server { | ||||
|     # Section for serving insecure HTTP, note that this is suitable for | ||||
|     # OCSP, SCEP, CRL-s etc which is already covered by PKI protection mechanisms. | ||||
|     # This also solves the chicken-and-egg problem of deploying the certificates | ||||
|  | ||||
|     server_name {{ common_name }}; | ||||
|     listen 80 default_server; | ||||
| #    rewrite ^ https://$server_name$request_uri? permanent; | ||||
| #} | ||||
|  | ||||
| #server { | ||||
| #    server_name {{ common_name }}; | ||||
| #    listen 443 ssl http2 default_server; | ||||
| #    add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;"; | ||||
| #    ssl_certificate /etc/letsencrypt/live/{{common_name}}/fullchain.pem; | ||||
| #    ssl_certificate_key /etc/letsencrypt/live/{{common_name}}/privkey.pem; | ||||
|  | ||||
|     root {{static_path}}; | ||||
|  | ||||
|     # Basic DoS prevention measures | ||||
|     limit_conn addr 10; | ||||
|     client_body_timeout 5s; | ||||
|     client_header_timeout 5s; | ||||
|  | ||||
|     # Proxy pass to backend | ||||
|     location /api/ { | ||||
|         proxy_pass http://127.0.1.1:8080/api/; | ||||
|         proxy_set_header Host $host; | ||||
|         proxy_set_header X-Real-IP $remote_addr; | ||||
|         proxy_connect_timeout 600; | ||||
|         proxy_send_timeout 600; | ||||
|         proxy_read_timeout 600; | ||||
|         send_timeout 600; | ||||
|         limit_req zone=api burst=5; | ||||
|     } | ||||
|  | ||||
|     # This is for Let's Encrypt | ||||
|     location /.well-known/ { | ||||
|         alias /var/www/html/.well-known/; | ||||
|     } | ||||
|     # Path to static files | ||||
|     root {{static_path}}; | ||||
|  | ||||
|     # Rewrite /cgi-bin/pkiclient.exe to /api/scep for SCEP protocol | ||||
|     location /cgi-bin/pkiclient.exe { | ||||
|         rewrite /cgi-bin/pkiclient.exe /api/scep/ last; | ||||
|     } | ||||
|  | ||||
| {% if not push_server %} | ||||
|     # This only works with nchan, for Debian 9 just apt install libnginx-mod-nchan | ||||
|     # For Ubuntu and older Debian releases install nchan from https://nchan.io/ | ||||
|  | ||||
|     # Long poll for CSR submission | ||||
|     location ~ "^/lp/sub/(.*)" { | ||||
|         nchan_channel_id $1; | ||||
|         nchan_subscriber longpoll; | ||||
|     } | ||||
|  | ||||
|     # Comment everything below in this server definition if you're using HTTPS | ||||
|  | ||||
|     # Event source for web interface | ||||
|     location ~ "^/ev/sub/(.*)" { | ||||
|         nchan_channel_id $1; | ||||
|         nchan_subscriber eventsource; | ||||
|     } | ||||
| {% endif %} | ||||
|  | ||||
|     # Uncomment following to enable HTTPS | ||||
|     #rewrite ^/$ https://$server_name$request_uri? permanent; | ||||
| } | ||||
|  | ||||
| server { | ||||
|     # Section for accessing web interface over HTTPS | ||||
|     listen 443 ssl http2 default_server; | ||||
|     server_name {{ common_name }}; | ||||
|  | ||||
|     # HSTS header below should make sure web interface will be accessed over HTTPS only | ||||
|     # once it has been configured | ||||
|     add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;"; | ||||
|  | ||||
|     # Proxy pass to backend | ||||
|     location /api/ { | ||||
|         proxy_pass http://127.0.1.1:8080/api/; | ||||
|         limit_req zone=api burst=5; | ||||
|     } | ||||
|  | ||||
|     # Path to static files | ||||
|     root {{static_path}}; | ||||
|  | ||||
|     # This is for Let's Encrypt enroll/renewal | ||||
|     location /.well-known/ { | ||||
|         alias /var/www/html/.well-known/; | ||||
|     } | ||||
|  | ||||
|     # Event stream for pushinge events to web browsers | ||||
|     location ~ "^/ev/sub/(.*)" { | ||||
|         nchan_channel_id $1; | ||||
|         nchan_subscriber eventsource; | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| server { | ||||
|     # Section for certificate authenticated HTTPS clients, | ||||
|     # for submitting information to CA eg. leases | ||||
|     # and for delivering scripts to clients | ||||
|  | ||||
|     server_name {{ common_name }}; | ||||
|     listen 8443 ssl http2; | ||||
|  | ||||
|     # Require client authentication with certificate | ||||
|     ssl_verify_client on; | ||||
|     ssl_client_certificate /var/lib/certidude/{{ common_name }}/ca_crt.pem; | ||||
|  | ||||
|     # Proxy pass to backend | ||||
|     location /api/ { | ||||
|         proxy_pass http://127.0.1.1:8080/api/; | ||||
|         limit_req zone=api burst=5; | ||||
|     } | ||||
|  | ||||
|     # Long poll | ||||
|     location ~ "^/lp/sub/(.*)" { | ||||
|         nchan_channel_id $1; | ||||
|         nchan_subscriber longpoll; | ||||
|     } | ||||
| } | ||||
|  | ||||
| {% if not push_server %} | ||||
| @@ -75,13 +142,11 @@ server { | ||||
|     location ~ "^/lp/pub/(.*)" { | ||||
|         nchan_publisher; | ||||
|         nchan_channel_id $1; | ||||
|         nchan_message_buffer_length 0; | ||||
|     } | ||||
|  | ||||
|     location ~ "^/ev/pub/(.*)" { | ||||
|         nchan_publisher; | ||||
|         nchan_channel_id $1; | ||||
|         nchan_message_buffer_length 0; | ||||
|     } | ||||
| } | ||||
| {% endif %} | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| click>=6.7 | ||||
| configparser>=3.5.0 | ||||
| certbuilder | ||||
| crlbuilder | ||||
| oscrypto | ||||
|   | ||||
| @@ -80,12 +80,6 @@ def clean_client(): | ||||
|  | ||||
|  | ||||
| def clean_server(): | ||||
|     if os.path.exists("/run/certidude/signer.pid"): | ||||
|         with open("/run/certidude/signer.pid") as fh: | ||||
|             try: | ||||
|                 os.kill(int(fh.read()), 15) | ||||
|             except OSError: | ||||
|                 pass | ||||
|     if os.path.exists("/run/certidude/server.pid"): | ||||
|         with open("/run/certidude/server.pid") as fh: | ||||
|             try: | ||||
| @@ -239,16 +233,18 @@ def test_cli_setup_authority(): | ||||
|     os.setgid(0) # Restore GID | ||||
|     os.umask(0022) | ||||
|  | ||||
|     # Make sure nginx is running | ||||
|     assert not result.exception, result.output | ||||
|     assert os.getuid() == 0 and os.getgid() == 0, "Serve dropped permissions incorrectly!" | ||||
|     assert os.system("nginx -t") == 0, "invalid nginx configuration" | ||||
|     os.system("service nginx restart") | ||||
|     assert os.path.exists("/run/nginx.pid"), "nginx wasn't started up properly" | ||||
|  | ||||
|     from certidude import config, authority, auth, user | ||||
|     assert authority.ca_cert.serial_number >= 0x100000000000000000000000000000000000000 | ||||
|     assert authority.ca_cert.serial_number <= 0xfffffffffffffffffffffffffffffffffffffff | ||||
|     assert authority.ca_cert.not_valid_before < datetime.now() | ||||
|     assert authority.ca_cert.not_valid_after > datetime.now() + timedelta(days=7000) | ||||
|     assert authority.certificate.serial_number >= 0x100000000000000000000000000000000000000 | ||||
|     assert authority.certificate.serial_number <= 0xfffffffffffffffffffffffffffffffffffffff | ||||
|     assert authority.certificate["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None) < datetime.utcnow() | ||||
|     assert authority.certificate["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None) > datetime.utcnow() + timedelta(days=7000) | ||||
|     assert authority.server_flags("lauri@fedora-123") == False | ||||
|     assert authority.server_flags("fedora-123") == False | ||||
|     assert authority.server_flags("vpn.example.lan") == True | ||||
| @@ -412,12 +408,12 @@ def test_cli_setup_authority(): | ||||
|     assert not result.exception, result.output | ||||
|  | ||||
|     # Test sign API call | ||||
|     r = client().simulate_patch("/api/request/test/") | ||||
|     r = client().simulate_post("/api/request/test/") | ||||
|     assert r.status_code == 401, r.text | ||||
|     r = client().simulate_patch("/api/request/test/", | ||||
|     r = client().simulate_post("/api/request/test/", | ||||
|         headers={"Authorization":usertoken}) | ||||
|     assert r.status_code == 403, r.text | ||||
|     r = client().simulate_patch("/api/request/test/", | ||||
|     r = client().simulate_post("/api/request/test/", | ||||
|         headers={"Authorization":admintoken}) | ||||
|     assert r.status_code == 201, r.text | ||||
|     assert "Signed " in inbox.pop(), inbox | ||||
| @@ -476,7 +472,7 @@ def test_cli_setup_authority(): | ||||
|     # Test revocations API call | ||||
|     r = client().simulate_get("/api/revoked/", | ||||
|         headers={"Accept":"application/x-pem-file"}) | ||||
|     assert r.status_code == 200, r.text # if this breaks certidude serve has no access to signer socket | ||||
|     assert r.status_code == 200, r.text | ||||
|     assert r.headers.get('content-type') == "application/x-pem-file" | ||||
|  | ||||
|     r = client().simulate_get("/api/revoked/") | ||||
| @@ -672,29 +668,11 @@ def test_cli_setup_authority(): | ||||
|     assert "/ev/sub/" in r.text, r.text | ||||
|     assert r.json, r.text | ||||
|     assert r.json.get("authority"), r.text | ||||
|     assert r.json.get("authority").get("events"), r.text | ||||
|  | ||||
|  | ||||
|     ################################# | ||||
|     ### Subscribe to event source ### | ||||
|     ################################# | ||||
|  | ||||
|     ev_pid = os.fork() | ||||
|     if not ev_pid: | ||||
|         url = r.json.get("authority").get("events") | ||||
|         if url.startswith("/"): # Expand URL | ||||
|             url = "http://ca.example.lan" + url | ||||
|         r = requests.get(url, headers={"Accept": "text/event-stream"}, stream=True) | ||||
|         lines = ["data: userbot@fedora-15417dc5", "event: request-signed"] # In reverse order! | ||||
|         assert r.status_code == 200, r.text | ||||
|         for line in r.iter_lines(): | ||||
|             if not line or line.startswith("id:") or line.startswith(":"): | ||||
|                 continue | ||||
|             assert line == lines.pop(), line | ||||
|             if not lines: | ||||
|                 return | ||||
|         assert False, r.text # This should not happen | ||||
|         return | ||||
|     ev_url = r.json.get("authority").get("events") | ||||
|     assert ev_url, r.text | ||||
|     if ev_url.startswith("/"): # Expand URL | ||||
|         ev_url = "http://ca.example.lan" + ev_url | ||||
|     assert ev_url.startswith("http://ca.example.lan/ev/sub/") | ||||
|  | ||||
|  | ||||
|     ####################### | ||||
| @@ -704,6 +682,7 @@ def test_cli_setup_authority(): | ||||
|     r = client().simulate_post("/api/token/") | ||||
|     assert r.status_code == 404, r.text | ||||
|  | ||||
|     """ | ||||
|     config.BUNDLE_FORMAT = "ovpn" | ||||
|     config.USER_ENROLLMENT_ALLOWED = True | ||||
|  | ||||
| @@ -734,6 +713,7 @@ def test_cli_setup_authority(): | ||||
|     assert r2.status_code == 200 # token consumed by anyone on unknown device | ||||
|     assert r2.headers.get('content-type') == "application/x-pkcs12" | ||||
|     assert "Signed " in inbox.pop(), inbox | ||||
|     """ | ||||
|  | ||||
|     # Beyond this point don't use client() | ||||
|     const.STORAGE_PATH = "/tmp/" | ||||
| @@ -765,12 +745,13 @@ def test_cli_setup_authority(): | ||||
|     result = runner.invoke(cli, ["request", "--no-wait"]) | ||||
|     assert not result.exception, result.output | ||||
|     assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output | ||||
|     assert "refused to sign" in result.output, result.output | ||||
|     assert "(Autosign failed, only client certificates allowed to be signed automatically)" in result.output, result.output | ||||
|  | ||||
|     child_pid = os.fork() | ||||
|     if not child_pid: | ||||
|         result = runner.invoke(cli, ['sign', 'www.example.lan']) | ||||
|         result = runner.invoke(cli, ["sign", "www.example.lan"]) | ||||
|         assert not result.exception, result.output | ||||
|         assert "Publishing request-signed event 'www.example.lan' on http://localhost/ev/pub/" in result.output, result.output | ||||
|         return | ||||
|     else: | ||||
|         os.waitpid(child_pid, 0) | ||||
| @@ -785,13 +766,10 @@ def test_cli_setup_authority(): | ||||
|     assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output | ||||
|     #assert "Writing certificate to:" in result.output, result.output | ||||
|     assert "Attached renewal signature" in result.output, result.output | ||||
|     #assert "refused to sign immideately" not in result.output, result.output | ||||
|  | ||||
|     # Test nginx setup | ||||
|     assert os.system("nginx -t") == 0, "Generated nginx config was invalid" | ||||
|  | ||||
|     # TODO: test client verification with curl | ||||
|  | ||||
|  | ||||
|     ############### | ||||
|     ### OpenVPN ### | ||||
| @@ -818,13 +796,17 @@ def test_cli_setup_authority(): | ||||
|  | ||||
|     result = runner.invoke(cli, ["request", "--no-wait"]) | ||||
|     assert not result.exception, result.output | ||||
|     assert "(Autosign failed, only client certificates allowed to be signed automatically)" in result.output, result.output | ||||
|     assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output | ||||
|  | ||||
|     assert not os.path.exists("/var/lib/certidude/ca.example.lan/signed/vpn.example.lan.pem") | ||||
|  | ||||
|     child_pid = os.fork() | ||||
|     if not child_pid: | ||||
|         result = runner.invoke(cli, ['sign', 'vpn.example.lan']) | ||||
|         assert not os.path.exists("/var/lib/certidude/ca.example.lan/signed/vpn.example.lan.pem") | ||||
|         result = runner.invoke(cli, ["sign", "vpn.example.lan"]) | ||||
|         assert not result.exception, result.output | ||||
|         assert "overwrit" not in result.output, result.output | ||||
|         assert "Publishing request-signed event 'vpn.example.lan' on http://localhost/ev/pub/" in result.output, result.output | ||||
|         return | ||||
|     else: | ||||
|         os.waitpid(child_pid, 0) | ||||
| @@ -859,10 +841,164 @@ def test_cli_setup_authority(): | ||||
|     # TODO: Check that tunnel interfaces came up, perhaps try to ping? | ||||
|     # TODO: assert key, req, cert paths were included correctly in OpenVPN config | ||||
|  | ||||
|     clean_client() | ||||
|  | ||||
|     ############### | ||||
|     result = runner.invoke(cli, ['setup', 'openvpn', 'networkmanager', "-cn", "roadwarrior3", "ca.example.lan", "vpn.example.lan"]) | ||||
|     assert not result.exception, result.output | ||||
|  | ||||
|     with open("/etc/certidude/client.conf", "a") as fh: | ||||
|         fh.write("insecure = true\n") | ||||
|  | ||||
|     result = runner.invoke(cli, ["request", "--no-wait"]) | ||||
|     assert not result.exception, result.output | ||||
|     assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output | ||||
|     assert "Writing certificate to:" in result.output, result.output | ||||
|  | ||||
|  | ||||
|     ################################# | ||||
|     ### Subscribe to event source ### | ||||
|     ################################# | ||||
|  | ||||
|     ev_pid = os.fork() | ||||
|     if not ev_pid: | ||||
|         r = requests.get(ev_url, headers={"Accept": "text/event-stream"}, stream=True) | ||||
|         assert r.status_code == 200, r.text | ||||
|         i = r.iter_lines() | ||||
|         assert i.next() == ": hi" | ||||
|         assert not i.next() | ||||
|  | ||||
|         # IPSec gateway below | ||||
|         assert i.next() == "event: log-entry", i.next() | ||||
|         assert i.next().startswith("id:") | ||||
|         assert i.next().startswith('data: {"message": "Served CA certificate ') | ||||
|         assert not i.next() | ||||
|  | ||||
|         assert i.next() == "event: log-entry", i.next() | ||||
|         assert i.next().startswith("id:") | ||||
|         assert i.next().startswith('data: {"message": "Serving revocation list (PEM)') | ||||
|         assert not i.next() | ||||
|  | ||||
|         assert i.next() == "event: log-entry", i.next() # FIXME | ||||
|         assert i.next().startswith("id:") | ||||
|         assert i.next().startswith('data: {"message": "Serving revocation list (PEM)') | ||||
|         assert not i.next() | ||||
|  | ||||
|         assert i.next() == "event: request-submitted", "%s; %s" % (i.next(), i.next()) | ||||
|         assert i.next().startswith("id:") | ||||
|         assert i.next() == "data: ipsec.example.lan" | ||||
|         assert not i.next() | ||||
|  | ||||
|         assert i.next() == "event: log-entry", i.next() | ||||
|         assert i.next().startswith("id:") | ||||
|         assert i.next().startswith('data: {"message": "Stored signing request ipsec.example.lan ') | ||||
|         assert not i.next() | ||||
|  | ||||
|         assert i.next() == "event: log-entry", i.next() # FIXME | ||||
|         assert i.next().startswith("id:") | ||||
|         assert i.next().startswith('data: {"message": "Stored signing request ipsec.example.lan ') | ||||
|         assert not i.next() | ||||
|  | ||||
|         assert i.next() == "event: request-signed" | ||||
|         assert i.next().startswith("id:") | ||||
|         assert i.next().startswith('data: ipsec.example.lan') | ||||
|         assert not i.next() | ||||
|  | ||||
|         assert i.next() == "event: log-entry", i.next() | ||||
|         assert i.next().startswith("id:") | ||||
|         assert i.next().startswith('data: {"message": "Serving revocation list (PEM)') | ||||
|         assert not i.next() | ||||
|  | ||||
|         assert i.next() == "event: log-entry", i.next() # FIXME | ||||
|         assert i.next().startswith("id:") | ||||
|         assert i.next().startswith('data: {"message": "Serving revocation list (PEM)') | ||||
|         assert not i.next() | ||||
|  | ||||
|         assert i.next() == "event: log-entry", i.next() | ||||
|         assert i.next().startswith("id:") | ||||
|         assert i.next().startswith('data: {"message": "Served certificate ipsec.example.lan') | ||||
|         assert not i.next() | ||||
|  | ||||
|         assert i.next() == "event: log-entry", i.next() # FIXME | ||||
|         assert i.next().startswith("id:") | ||||
|         assert i.next().startswith('data: {"message": "Served certificate ipsec.example.lan') | ||||
|         assert not i.next() | ||||
|  | ||||
|         # IPsec client as service enroll | ||||
|         assert i.next() == "event: log-entry", i.next() | ||||
|         assert i.next().startswith("id:") | ||||
|         assert i.next().startswith('data: {"message": "Serving revocation list (PEM)') | ||||
|         assert not i.next() | ||||
|  | ||||
|         assert i.next() == "event: log-entry", i.next() # FIXME | ||||
|         assert i.next().startswith("id:") | ||||
|         assert i.next().startswith('data: {"message": "Serving revocation list (PEM)') | ||||
|         assert not i.next() | ||||
|  | ||||
|         assert i.next() == "event: request-signed", i.next() | ||||
|         assert i.next().startswith("id:") | ||||
|         assert i.next().startswith('data: roadwarrior2') | ||||
|         assert not i.next() | ||||
|  | ||||
|         assert i.next() == "event: log-entry", i.next() | ||||
|         assert i.next().startswith("id:") | ||||
|         assert i.next().startswith('data: {"message": "Autosigned roadwarrior2') | ||||
|         assert not i.next() | ||||
|  | ||||
|         assert i.next() == "event: log-entry", i.next() # FIXME | ||||
|         assert i.next().startswith("id:") | ||||
|         assert i.next().startswith('data: {"message": "Autosigned roadwarrior2') | ||||
|         assert not i.next() | ||||
|  | ||||
|  | ||||
|  | ||||
|         # IPSec client using Networkmanger enroll | ||||
|         assert i.next() == "event: log-entry", i.next() | ||||
|         assert i.next().startswith("id:") | ||||
|         assert i.next().startswith('data: {"message": "Served CA certificate ') | ||||
|         assert not i.next() | ||||
|  | ||||
|         assert i.next() == "event: log-entry", i.next() | ||||
|         assert i.next().startswith("id:") | ||||
|         assert i.next().startswith('data: {"message": "Serving revocation list (PEM)') | ||||
|         assert not i.next() | ||||
|  | ||||
|         assert i.next() == "event: log-entry", i.next() # FIXME | ||||
|         assert i.next().startswith("id:") | ||||
|         assert i.next().startswith('data: {"message": "Serving revocation list (PEM)') | ||||
|         assert not i.next() | ||||
|  | ||||
|         assert i.next() == "event: request-signed", i.next() | ||||
|         assert i.next().startswith("id:") | ||||
|         assert i.next().startswith('data: roadwarrior4') | ||||
|         assert not i.next() | ||||
|  | ||||
|         assert i.next() == "event: log-entry", i.next() | ||||
|         assert i.next().startswith("id:") | ||||
|         assert i.next().startswith('data: {"message": "Autosigned roadwarrior4') | ||||
|         assert not i.next() | ||||
|  | ||||
|         assert i.next() == "event: log-entry", i.next() # FIXME | ||||
|         assert i.next().startswith("id:") | ||||
|         assert i.next().startswith('data: {"message": "Autosigned roadwarrior4') | ||||
|         assert not i.next() | ||||
|  | ||||
|  | ||||
|         # Revoke | ||||
|  | ||||
|         assert i.next() == "event: certificate-revoked", i.next() # why?! | ||||
|         assert i.next().startswith("id:") | ||||
|         assert i.next().startswith('data: roadwarrior4') | ||||
|         assert not i.next() | ||||
|  | ||||
|  | ||||
|         return | ||||
|  | ||||
|  | ||||
|     ############# | ||||
|     ### IPSec ### | ||||
|     ############### | ||||
|     ############# | ||||
|  | ||||
|     # Setup gateway | ||||
|  | ||||
|     clean_client() | ||||
|  | ||||
| @@ -882,11 +1018,15 @@ def test_cli_setup_authority(): | ||||
|     result = runner.invoke(cli, ["request", "--no-wait"]) | ||||
|     assert not result.exception, result.output | ||||
|     assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output | ||||
|     assert not os.path.exists("/var/lib/certidude/ca.example.lan/signed/ipsec.example.lan.pem") | ||||
|  | ||||
|     child_pid = os.fork() | ||||
|     if not child_pid: | ||||
|         result = runner.invoke(cli, ['sign', 'ipsec.example.lan']) | ||||
|         assert not os.path.exists("/var/lib/certidude/ca.example.lan/signed/ipsec.example.lan.pem") | ||||
|         result = runner.invoke(cli, ["sign", "ipsec.example.lan"]) | ||||
|         assert not result.exception, result.output | ||||
|         assert "overwrit" not in result.output, result.output | ||||
|         assert "Publishing request-signed event 'ipsec.example.lan' on http://localhost/ev/pub/" in result.output, result.output | ||||
|         return | ||||
|     else: | ||||
|         os.waitpid(child_pid, 0) | ||||
| @@ -898,7 +1038,8 @@ def test_cli_setup_authority(): | ||||
|     assert "Writing certificate to:" in result.output, result.output | ||||
|     assert os.path.exists("/tmp/ca.example.lan/server_cert.pem") | ||||
|  | ||||
|     # Reset config | ||||
|     # IPSec client as service | ||||
|  | ||||
|     os.unlink("/etc/certidude/client.conf") | ||||
|     os.unlink("/etc/certidude/services.conf") | ||||
|  | ||||
| @@ -917,23 +1058,7 @@ def test_cli_setup_authority(): | ||||
|  | ||||
|     assert "Writing certificate to:" in result.output, result.output | ||||
|  | ||||
|  | ||||
|     ###################### | ||||
|     ### NetworkManager ### | ||||
|     ###################### | ||||
|  | ||||
|     clean_client() | ||||
|  | ||||
|     result = runner.invoke(cli, ['setup', 'openvpn', 'networkmanager', "-cn", "roadwarrior3", "ca.example.lan", "vpn.example.lan"]) | ||||
|     assert not result.exception, result.output | ||||
|  | ||||
|     with open("/etc/certidude/client.conf", "a") as fh: | ||||
|         fh.write("insecure = true\n") | ||||
|  | ||||
|     result = runner.invoke(cli, ["request", "--no-wait"]) | ||||
|     assert not result.exception, result.output | ||||
|     assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output | ||||
|     assert "Writing certificate to:" in result.output, result.output | ||||
|     # IPSec using NetworkManager | ||||
|  | ||||
|     clean_client() | ||||
|  | ||||
| @@ -1158,11 +1283,15 @@ def test_cli_setup_authority(): | ||||
|     ################## | ||||
|  | ||||
|     os.umask(0022) | ||||
|     assert not os.system("git clone  https://github.com/certnanny/sscep /tmp/sscep") | ||||
|     assert not os.system("cd /tmp/sscep && ./Configure && make sscep_dyn") | ||||
|     if not os.path.exists("/tmp/sscep"): | ||||
|         assert not os.system("git clone  https://github.com/certnanny/sscep /tmp/sscep") | ||||
|     if not os.path.exists("/tmp/sscep/sscep_dyn"): | ||||
|         assert not os.system("cd /tmp/sscep && ./Configure && make sscep_dyn") | ||||
|     assert not os.system("/tmp/sscep/sscep_dyn getca -c /tmp/sscep/ca.pem -u http://ca.example.lan/cgi-bin/pkiclient.exe") | ||||
|     assert not os.system("openssl genrsa -out /tmp/key.pem 1024") | ||||
|     assert not os.system("echo '.\n.\n.\n.\n.\ntest8\n\n\n\n' | openssl req -new -sha256 -key /tmp/key.pem -out /tmp/req.pem") | ||||
|     if not os.path.exists("/tmp/key.pem"): | ||||
|         assert not os.system("openssl genrsa -out /tmp/key.pem 1024") | ||||
|     if not os.path.exists("/tmp/req.pem"): | ||||
|         assert not os.system("echo '.\n.\n.\n.\n.\ntest8\n\n\n\n' | openssl req -new -sha256 -key /tmp/key.pem -out /tmp/req.pem") | ||||
|     assert not os.system("/tmp/sscep/sscep_dyn enroll -c /tmp/sscep/ca.pem -u http://ca.example.lan/cgi-bin/pkiclient.exe -k /tmp/key.pem -r /tmp/req.pem -l /tmp/cert.pem") | ||||
|     # TODO: test e-mails at this point | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user