mirror of
				https://github.com/laurivosandi/certidude
				synced 2025-10-30 17:09:19 +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 | Introduction | ||||||
| ------------ | ------------ | ||||||
|  |  | ||||||
| Certidude is a novel X.509 Certificate Authority management tool | Certidude is a minimalist X.509 Certificate Authority management tool | ||||||
| with privilege isolation mechanism and Kerberos authentication | with Kerberos authentication mainly designed for OpenVPN gateway operators to make | ||||||
| mainly designed for OpenVPN gateway operators to make |  | ||||||
| VPN client setup on laptops, desktops and mobile devices as painless as possible. | VPN client setup on laptops, desktops and mobile devices as painless as possible. | ||||||
|  |  | ||||||
| .. figure:: doc/certidude.png | .. 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 |   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. |   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 | Features | ||||||
| -------- | -------- | ||||||
| @@ -68,16 +60,14 @@ Features | |||||||
| Common: | Common: | ||||||
|  |  | ||||||
| * Standard request, sign, revoke workflow via web interface. | * Standard request, sign, revoke workflow via web interface. | ||||||
| * Kerberos and basic auth based web interface authentication. | * `OCSP <https://tools.ietf.org/html/rfc4557>`_ and `SCEP <https://tools.ietf.org/html/draft-nourse-scep-23>`_ support. | ||||||
| * Preliminary `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. | * PAM and Active Directory compliant authentication backends: Kerberos single sign-on, LDAP simple bind. | ||||||
| * POSIX groups and Active Directory (LDAP) group membership based authorization. | * POSIX groups and Active Directory (LDAP) group membership based authorization. | ||||||
| * Server-side command-line interface, check out ``certidude list``, ``certidude sign`` and ``certidude revoke``. | * 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. | * Certificate serial numbers are intentionally randomized to avoid leaking information about business practices. | ||||||
| * Server-side events support via `nchan <https://nchan.slact.net/>`_. | * 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: | Virtual private networking: | ||||||
|  |  | ||||||
| @@ -95,9 +85,7 @@ HTTPS: | |||||||
| TODO | 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. | * Use `pki.js <https://pkijs.org/>`_ for generating keypair in the browser when claiming a token. | ||||||
| * Signer process logging. |  | ||||||
|  |  | ||||||
|  |  | ||||||
| Install | Install | ||||||
| @@ -110,7 +98,8 @@ System dependencies for Ubuntu 16.04: | |||||||
|  |  | ||||||
| .. code:: bash | .. code:: bash | ||||||
|  |  | ||||||
|     apt install -y python python-cffi python-click python-configparser \ |     apt install -y | ||||||
|  |         python-click python-configparser \ | ||||||
|         python-humanize \ |         python-humanize \ | ||||||
|         python-ipaddress python-jinja2 python-ldap python-markdown \ |         python-ipaddress python-jinja2 python-ldap python-markdown \ | ||||||
|         python-mimeparse python-mysql.connector python-openssl python-pip \ |         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 |     yum install redhat-rpm-config python-devel openssl-devel openldap-devel | ||||||
|  |  | ||||||
| At the moment package at PyPI is rather outdated. | 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 | Setting up authority | ||||||
|   | |||||||
| @@ -13,7 +13,6 @@ from certidude import authority, mailer | |||||||
| from certidude.auth import login_required, authorize_admin | from certidude.auth import login_required, authorize_admin | ||||||
| from certidude.user import User | from certidude.user import User | ||||||
| from certidude.decorators import serialize, csrf_protection | from certidude.decorators import serialize, csrf_protection | ||||||
| from cryptography.x509.oid import NameOID |  | ||||||
| from certidude import const, config | from certidude import const, config | ||||||
|  |  | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
| @@ -82,8 +81,8 @@ class SessionResource(object): | |||||||
|                     common_name = common_name, |                     common_name = common_name, | ||||||
|                     server = server, |                     server = server, | ||||||
|                     # TODO: key type, key length, key exponent, key modulo |                     # TODO: key type, key length, key exponent, key modulo | ||||||
|                     signed = obj.not_valid_before, |                     signed = obj["tbs_certificate"]["validity"]["not_before"].native, | ||||||
|                     expires = obj.not_valid_after, |                     expires = obj["tbs_certificate"]["validity"]["not_after"].native, | ||||||
|                     sha256sum = hashlib.sha256(buf).hexdigest(), |                     sha256sum = hashlib.sha256(buf).hexdigest(), | ||||||
|                     lease = lease, |                     lease = lease, | ||||||
|                     tags = tags, |                     tags = tags, | ||||||
| @@ -108,8 +107,7 @@ class SessionResource(object): | |||||||
|                     offline = 600, # Seconds from last seen activity to consider lease offline, OpenVPN reneg-sec option |                     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 |                     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( |                 common_name = authority.certificate.subject.native["common_name"], | ||||||
|                     NameOID.COMMON_NAME)[0].value, |  | ||||||
|                 mailer = dict( |                 mailer = dict( | ||||||
|                     name = config.MAILER_NAME, |                     name = config.MAILER_NAME, | ||||||
|                     address = config.MAILER_ADDRESS |                     address = config.MAILER_ADDRESS | ||||||
|   | |||||||
| @@ -33,7 +33,7 @@ class LeaseResource(object): | |||||||
|         # TODO: verify signature |         # TODO: verify signature | ||||||
|         common_name = req.get_param("client", required=True) |         common_name = req.get_param("client", required=True) | ||||||
|         path, buf, cert = authority.get_signed(common_name) # TODO: catch exceptions |         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") |             raise falcon.HTTPForbidden("Forbidden", "Invalid serial number supplied") | ||||||
|  |  | ||||||
|         xattr.setxattr(path, "user.lease.outer_address", req.get_param("outer_address", required=True).encode("ascii")) |         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.startswith("../") | ||||||
|                 assert link_target.endswith(".pem") |                 assert link_target.endswith(".pem") | ||||||
|                 path, buf, cert = authority.get_signed(link_target[3:-4]) |                 path, buf, cert = authority.get_signed(link_target[3:-4]) | ||||||
|                 if serial != cert.serial: |                 if serial != cert.serial_number: | ||||||
|                     raise EnvironmentError("integrity check failed") |                     raise EnvironmentError("integrity check failed") | ||||||
|                 status = ocsp.CertStatus(name='good', value=None) |                 status = ocsp.CertStatus(name='good', value=None) | ||||||
|             except EnvironmentError: |             except EnvironmentError: | ||||||
| @@ -94,9 +94,13 @@ class OCSPResource(object): | |||||||
|                 'response_type': u"basic_ocsp_response", |                 'response_type': u"basic_ocsp_response", | ||||||
|                 'response': { |                 'response': { | ||||||
|                     'tbs_response_data': response_data, |                     'tbs_response_data': response_data, | ||||||
|  |                     'certs': [server_certificate.asn1], | ||||||
|                     'signature_algorithm': {'algorithm': u"sha1_rsa"}, |                     'signature_algorithm': {'algorithm': u"sha1_rsa"}, | ||||||
|                     'signature': b64decode(authority.signer_exec("sign-pkcs7", b64encode(response_data.dump()))), |                     'signature': asymmetric.rsa_pkcs1v15_sign( | ||||||
|                     'certs': [server_certificate.asn1] |                         authority.private_key, | ||||||
|  |                         response_data.dump(), | ||||||
|  |                         "sha1" | ||||||
|  |                     ) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         }).dump() |         }).dump() | ||||||
|   | |||||||
| @@ -6,18 +6,16 @@ import ipaddress | |||||||
| import json | import json | ||||||
| import os | import os | ||||||
| import hashlib | import hashlib | ||||||
|  | from asn1crypto import pem | ||||||
|  | from asn1crypto.csr import CertificationRequest | ||||||
| from base64 import b64decode | from base64 import b64decode | ||||||
| from certidude import config, authority, push, errors | from certidude import config, authority, push, errors | ||||||
| from certidude.auth import login_required, login_optional, authorize_admin | from certidude.auth import login_required, login_optional, authorize_admin | ||||||
| from certidude.decorators import serialize, csrf_protection | from certidude.decorators import serialize, csrf_protection | ||||||
| from certidude.firewall import whitelist_subnets, whitelist_content_types | 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 datetime import datetime | ||||||
|  | from oscrypto import asymmetric | ||||||
|  | from oscrypto.errors import SignatureError | ||||||
| from xattr import getxattr | from xattr import getxattr | ||||||
|  |  | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
| @@ -35,19 +33,14 @@ class RequestListResource(object): | |||||||
|     @whitelist_content_types("application/pkcs10") |     @whitelist_content_types("application/pkcs10") | ||||||
|     def on_post(self, req, resp): |     def on_post(self, req, resp): | ||||||
|         """ |         """ | ||||||
|         Validate and parse certificate signing request |         Validate and parse certificate signing request, the RESTful way | ||||||
|         """ |         """ | ||||||
|         reasons = [] |         reasons = [] | ||||||
|         body = req.stream.read(req.content_length) |         body = req.stream.read(req.content_length).encode("ascii") | ||||||
|         csr = x509.load_pem_x509_csr(body, default_backend()) |  | ||||||
|         try: |         header, _, der_bytes = pem.unarmor(body) | ||||||
|             common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME) |         csr = CertificationRequest.load(der_bytes) | ||||||
|         except: # ValueError? |         common_name = csr["certification_request_info"]["subject"].native["common_name"] | ||||||
|             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!") |  | ||||||
|  |  | ||||||
|         """ |         """ | ||||||
|         Handle domain computer automatic enrollment |         Handle domain computer automatic enrollment | ||||||
| @@ -55,10 +48,10 @@ class RequestListResource(object): | |||||||
|         machine = req.context.get("machine") |         machine = req.context.get("machine") | ||||||
|         if machine: |         if machine: | ||||||
|             if config.MACHINE_ENROLLMENT_ALLOWED: |             if config.MACHINE_ENROLLMENT_ALLOWED: | ||||||
|                 if common_name.value != machine: |                 if common_name != machine: | ||||||
|                     raise falcon.HTTPBadRequest( |                     raise falcon.HTTPBadRequest( | ||||||
|                         "Bad request", |                         "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 |                 # Automatic enroll with Kerberos machine cerdentials | ||||||
|                 resp.set_header("Content-Type", "application/x-pem-file") |                 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 |         Attempt to renew certificate using currently valid key pair | ||||||
|         """ |         """ | ||||||
|         try: |         try: | ||||||
|             path, buf, cert = authority.get_signed(common_name.value) |             path, buf, cert = authority.get_signed(common_name) | ||||||
|         except EnvironmentError: |         except EnvironmentError: | ||||||
|             pass |             pass # No currently valid certificate for this common name | ||||||
|         else: |         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") |                 renewal_header = req.get_header("X-Renewal-Signature") | ||||||
|  |  | ||||||
|                 if not renewal_header: |                 if not renewal_header: | ||||||
|                     # No header supplied, redirect to signed API call |                     # No header supplied, redirect to signed API call | ||||||
|                     resp.status = falcon.HTTP_SEE_OTHER |                     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 |                     return | ||||||
|  |  | ||||||
|                 try: |                 try: | ||||||
|                     renewal_signature = b64decode(renewal_header) |                     renewal_signature = b64decode(renewal_header) | ||||||
|                 except TypeError, ValueError: |                 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") |                     reasons.append("Renewal failed, bad signature supplied") | ||||||
|                 else: |                 else: | ||||||
|                     try: |                     try: | ||||||
|                         verifier = cert.public_key().verifier( |                         asymmetric.rsa_pss_verify( | ||||||
|                             renewal_signature, |                             asymmetric.load_certificate(cert), | ||||||
|                             padding.PSS( |                             renewal_signature, buf + body, "sha512") | ||||||
|                                 mgf=padding.MGF1(hashes.SHA512()), |                     except SignatureError: | ||||||
|                                 salt_length=padding.PSS.MAX_LENGTH |                         logger.error(u"Renewal failed, invalid signature supplied for %s", common_name) | ||||||
|                             ), |  | ||||||
|                             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) |  | ||||||
|                         reasons.append("Renewal failed, invalid signature supplied") |                         reasons.append("Renewal failed, invalid signature supplied") | ||||||
|                     else: |                     else: | ||||||
|                         # At this point renewal signature was valid but we need to perform some extra checks |                         # At this point renewal signature was valid but we need to perform some extra checks | ||||||
|                         if datetime.utcnow() > cert.not_valid_after: |                         if datetime.utcnow() > expires: | ||||||
|                             logger.error(u"Renewal failed, current certificate for %s has expired", common_name.value) |                             logger.error(u"Renewal failed, current certificate for %s has expired", common_name) | ||||||
|                             reasons.append("Renewal failed, current certificate expired") |                             reasons.append("Renewal failed, current certificate expired") | ||||||
|                         elif not config.CERTIFICATE_RENEWAL_ALLOWED: |                         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") |                             reasons.append("Renewal requested, but not allowed by authority settings") | ||||||
|                         else: |                         else: | ||||||
|                             resp.set_header("Content-Type", "application/x-x509-user-cert") |                             resp.set_header("Content-Type", "application/x-x509-user-cert") | ||||||
|                             _, resp.body = authority._sign(csr, body, overwrite=True) |                             _, 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 |                             return | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -127,17 +116,17 @@ class RequestListResource(object): | |||||||
|         autosigning was requested and certificate can be automatically signed |         autosigning was requested and certificate can be automatically signed | ||||||
|         """ |         """ | ||||||
|         if req.get_param_as_bool("autosign"): |         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: |                 for subnet in config.AUTOSIGN_SUBNETS: | ||||||
|                     if req.context.get("remote_addr") in subnet: |                     if req.context.get("remote_addr") in subnet: | ||||||
|                         try: |                         try: | ||||||
|                             resp.set_header("Content-Type", "application/x-pem-file") |                             resp.set_header("Content-Type", "application/x-pem-file") | ||||||
|                             _, resp.body = authority._sign(csr, body) |                             _, 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 |                             return | ||||||
|                         except EnvironmentError: |                         except EnvironmentError: | ||||||
|                             logger.info(u"Autosign for %s from %s failed, signed certificate already exists", |                             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") |                             reasons.append("Autosign failed, signed certificate already exists") | ||||||
|                         break |                         break | ||||||
|                 else: |                 else: | ||||||
| @@ -147,7 +136,7 @@ class RequestListResource(object): | |||||||
|  |  | ||||||
|         # Attempt to save the request otherwise |         # Attempt to save the request otherwise | ||||||
|         try: |         try: | ||||||
|             request_path, _, _ = authority.store_request(body.decode("ascii"), |             request_path, _, _ = authority.store_request(body, | ||||||
|                 address=str(req.context.get("remote_addr"))) |                 address=str(req.context.get("remote_addr"))) | ||||||
|         except errors.RequestExists: |         except errors.RequestExists: | ||||||
|             reasons.append("Same request already uploaded exists") |             reasons.append("Same request already uploaded exists") | ||||||
| @@ -160,10 +149,10 @@ class RequestListResource(object): | |||||||
|                 "CSR with such CN already exists", |                 "CSR with such CN already exists", | ||||||
|                 "Will not overwrite existing certificate signing request, explicitly delete CSR and try again") |                 "Will not overwrite existing certificate signing request, explicitly delete CSR and try again") | ||||||
|         else: |         else: | ||||||
|             push.publish("request-submitted", common_name.value) |             push.publish("request-submitted", common_name) | ||||||
|  |  | ||||||
|         # Wait the certificate to be signed if waiting is requested |         # 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"): |         if req.get_param("wait"): | ||||||
|             # Redirect to nginx pub/sub |             # Redirect to nginx pub/sub | ||||||
|             url = config.LONG_POLL_SUBSCRIBE % hashlib.sha256(body).hexdigest() |             url = config.LONG_POLL_SUBSCRIBE % hashlib.sha256(body).hexdigest() | ||||||
| @@ -221,7 +210,7 @@ class RequestDetailResource(object): | |||||||
|     @csrf_protection |     @csrf_protection | ||||||
|     @login_required |     @login_required | ||||||
|     @authorize_admin |     @authorize_admin | ||||||
|     def on_patch(self, req, resp, cn): |     def on_post(self, req, resp, cn): | ||||||
|         """ |         """ | ||||||
|         Sign a certificate signing request |         Sign a certificate signing request | ||||||
|         """ |         """ | ||||||
|   | |||||||
| @@ -6,9 +6,6 @@ import logging | |||||||
| from certidude import const, config | from certidude import const, config | ||||||
| from certidude.authority import export_crl, list_revoked | from certidude.authority import export_crl, list_revoked | ||||||
| from certidude.firewall import whitelist_subnets | 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__) | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
| @@ -23,9 +20,8 @@ class RevocationListResource(object): | |||||||
|                 "Content-Disposition", |                 "Content-Disposition", | ||||||
|                 ("attachment; filename=%s.crl" % const.HOSTNAME).encode("ascii")) |                 ("attachment; filename=%s.crl" % const.HOSTNAME).encode("ascii")) | ||||||
|             # Convert PEM to DER |             # Convert PEM to DER | ||||||
|             logger.debug(u"Serving revocation list to %s in DER format", req.context.get("remote_addr")) |             logger.debug(u"Serving revocation list (DER) to %s", req.context.get("remote_addr")) | ||||||
|             resp.body = x509.load_pem_x509_crl(export_crl(), |             resp.body = export_crl(pem=False) | ||||||
|                 default_backend()).public_bytes(Encoding.DER) |  | ||||||
|         elif req.client_accepts("application/x-pem-file"): |         elif req.client_accepts("application/x-pem-file"): | ||||||
|             if req.get_param_as_bool("wait"): |             if req.get_param_as_bool("wait"): | ||||||
|                 url = config.LONG_POLL_SUBSCRIBE % "crl" |                 url = config.LONG_POLL_SUBSCRIBE % "crl" | ||||||
| @@ -38,7 +34,7 @@ class RevocationListResource(object): | |||||||
|                 resp.append_header( |                 resp.append_header( | ||||||
|                     "Content-Disposition", |                     "Content-Disposition", | ||||||
|                     ("attachment; filename=%s-crl.pem" % const.HOSTNAME).encode("ascii")) |                     ("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() |                 resp.body = export_crl() | ||||||
|         else: |         else: | ||||||
|             logger.debug(u"Client %s asked revocation list in unsupported format" % req.context.get("remote_addr")) |             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): |     def on_get(self, req, resp): | ||||||
|         operation = req.get_param("operation") |         operation = req.get_param("operation") | ||||||
|         if operation.lower() == "getcacert": |         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") |             resp.append_header("Content-Type", "application/x-x509-ca-cert") | ||||||
|             return |             return | ||||||
|  |  | ||||||
| @@ -125,14 +125,16 @@ class SCEPResource(object): | |||||||
|             encrypted_content = encrypted_content_info['encrypted_content'].native |             encrypted_content = encrypted_content_info['encrypted_content'].native | ||||||
|             recipient, = encrypted_envelope['recipient_infos'] |             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() |                 raise SCEPBadCertId() | ||||||
|  |  | ||||||
|             # Since CA private key is not directly readable here, we'll redirect it to signer socket |             # 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 |             if len(key) == 8: key = key * 3 # Convert DES to 3DES | ||||||
|             buf = symmetric.tripledes_cbc_pkcs5_decrypt(key, encrypted_content, iv) |             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) |             cert, buf = authority.sign(common_name, overwrite=True) | ||||||
|             signed_certificate = asymmetric.load_certificate(buf) |             signed_certificate = asymmetric.load_certificate(buf) | ||||||
|             content = signed_certificate.asn1.dump() |             content = signed_certificate.asn1.dump() | ||||||
| @@ -251,7 +253,11 @@ class SCEPResource(object): | |||||||
|                 }), |                 }), | ||||||
|                 'digest_algorithm': algos.DigestAlgorithm({'algorithm': u"sha1"}), |                 'digest_algorithm': algos.DigestAlgorithm({'algorithm': u"sha1"}), | ||||||
|                 'signature_algorithm': algos.SignedDigestAlgorithm({'algorithm': u"rsassa_pkcs1v15"}), |                 '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") |             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.set_header("Content-Disposition", ("attachment; filename=%s.json" % cn)) | ||||||
|             resp.body = json.dumps(dict( |             resp.body = json.dumps(dict( | ||||||
|                 common_name = cn, |                 common_name = cn, | ||||||
|                 serial_number = "%x" % cert.serial, |                 serial_number = "%x" % cert.serial_number, | ||||||
|                 signed = cert.not_valid_before.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z", |                 signed = cert["tbs_certificate"]["validity"]["not_before"].native.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", |                 expires = cert["tbs_certificate"]["validity"]["not_after"].native.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z", | ||||||
|                 sha256sum = hashlib.sha256(buf).hexdigest())) |                 sha256sum = hashlib.sha256(buf).hexdigest())) | ||||||
|             logger.debug(u"Served certificate %s to %s as application/json", |             logger.debug(u"Served certificate %s to %s as application/json", | ||||||
|                 cn, req.context.get("remote_addr")) |                 cn, req.context.get("remote_addr")) | ||||||
|   | |||||||
| @@ -1,23 +1,25 @@ | |||||||
| from __future__ import division, absolute_import, print_function | from __future__ import division, absolute_import, print_function | ||||||
| import click | import click | ||||||
| import os | import os | ||||||
| import random |  | ||||||
| import re | import re | ||||||
| import requests | import requests | ||||||
| import hashlib | import hashlib | ||||||
| import socket | import socket | ||||||
| from datetime import datetime, timedelta | from oscrypto import asymmetric | ||||||
| from cryptography.hazmat.backends import default_backend | from asn1crypto import pem, x509 | ||||||
| from cryptography import x509 | from asn1crypto.csr import CertificationRequest | ||||||
| from cryptography.x509.oid import NameOID, ExtensionOID, ExtendedKeyUsageOID | from certbuilder import CertificateBuilder | ||||||
| from cryptography.hazmat.primitives.asymmetric import rsa |  | ||||||
| from cryptography.hazmat.primitives import hashes, serialization |  | ||||||
| from cryptography.hazmat.primitives.serialization import Encoding |  | ||||||
| from certidude import config, push, mailer, const | from certidude import config, push, mailer, const | ||||||
| from certidude import errors | 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 jinja2 import Template | ||||||
|  | from random import SystemRandom | ||||||
| from xattr import getxattr, listxattr, setxattr | 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]))?$" | 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/ | # 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 | # Cache CA certificate | ||||||
|  |  | ||||||
| with open(config.AUTHORITY_CERTIFICATE_PATH) as fh: | with open(config.AUTHORITY_CERTIFICATE_PATH) as fh: | ||||||
|     ca_buf = fh.read() |     certificate_buf = fh.read() | ||||||
|     ca_cert = x509.load_pem_x509_certificate(ca_buf, default_backend()) |     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): | def get_request(common_name): | ||||||
|     if not re.match(RE_HOSTNAME, common_name): |     if not re.match(RE_HOSTNAME, common_name): | ||||||
| @@ -37,7 +45,8 @@ def get_request(common_name): | |||||||
|     try: |     try: | ||||||
|         with open(path) as fh: |         with open(path) as fh: | ||||||
|             buf = fh.read() |             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: |     except EnvironmentError: | ||||||
|         raise errors.RequestDoesNotExist("Certificate signing request file %s does not exist" % path) |         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") |     path = os.path.join(config.SIGNED_DIR, common_name + ".pem") | ||||||
|     with open(path) as fh: |     with open(path) as fh: | ||||||
|         buf = fh.read() |         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): | def get_revoked(serial): | ||||||
|     path = os.path.join(config.REVOKED_DIR, "%x.pem" % serial) |     path = os.path.join(config.REVOKED_DIR, "%x.pem" % serial) | ||||||
|     with open(path) as fh: |     with open(path) as fh: | ||||||
|         buf = fh.read() |         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) |             datetime.utcfromtimestamp(os.stat(path).st_ctime) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -85,20 +96,19 @@ def store_request(buf, overwrite=False, address="", user=""): | |||||||
|     if not buf: |     if not buf: | ||||||
|         raise ValueError("No signing request supplied") |         raise ValueError("No signing request supplied") | ||||||
|  |  | ||||||
|     if isinstance(buf, unicode): |     if pem.detect(buf): | ||||||
|         csr = x509.load_pem_x509_csr(buf.encode("ascii"), backend=default_backend()) |         header, _, der_bytes = pem.unarmor(buf) | ||||||
|     elif isinstance(buf, str): |         csr = CertificationRequest.load(der_bytes) | ||||||
|         csr = x509.load_der_x509_csr(buf, backend=default_backend()) |  | ||||||
|         buf = csr.public_bytes(Encoding.PEM) |  | ||||||
|     else: |     else: | ||||||
|         raise ValueError("Invalid type, expected str for PEM and bytes for DER") |         csr = CertificationRequest.load(buf) | ||||||
|     common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME) |         buf =  pem_armor_csr(csr) | ||||||
|     # TODO: validate common name again |  | ||||||
|  |  | ||||||
|     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") |         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 |     # 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) |             fh.write(buf) | ||||||
|         os.rename(request_path + ".part", request_path) |         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", |     mailer.send("request-stored.md", | ||||||
|         attachments=(attach_csr,), |         attachments=(attach_csr,), | ||||||
|         common_name=common_name.value) |         common_name=common_name) | ||||||
|     setxattr(request_path, "user.request.address", address) |     setxattr(request_path, "user.request.address", address) | ||||||
|     setxattr(request_path, "user.request.user", user) |     setxattr(request_path, "user.request.user", user) | ||||||
|     return request_path, csr, common_name.value |     return request_path, csr, common_name | ||||||
|  |  | ||||||
|  |  | ||||||
| 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 |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def revoke(common_name): | def revoke(common_name): | ||||||
| @@ -140,9 +136,9 @@ def revoke(common_name): | |||||||
|     Revoke valid certificate |     Revoke valid certificate | ||||||
|     """ |     """ | ||||||
|     signed_path, buf, cert = get_signed(common_name) |     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.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) |     push.publish("certificate-revoked", common_name) | ||||||
|  |  | ||||||
| @@ -155,7 +151,7 @@ def revoke(common_name): | |||||||
|     attach_cert = buf, "application/x-pem-file", common_name + ".crt" |     attach_cert = buf, "application/x-pem-file", common_name + ".crt" | ||||||
|     mailer.send("certificate-revoked.md", |     mailer.send("certificate-revoked.md", | ||||||
|         attachments=(attach_cert,), |         attachments=(attach_cert,), | ||||||
|         serial_hex="%x" % cert.serial, |         serial_hex="%x" % cert.serial_number, | ||||||
|         common_name=common_name) |         common_name=common_name) | ||||||
|     return revoked_path |     return revoked_path | ||||||
|  |  | ||||||
| @@ -186,12 +182,13 @@ def _list_certificates(directory): | |||||||
|             path = os.path.join(directory, filename) |             path = os.path.join(directory, filename) | ||||||
|             with open(path) as fh: |             with open(path) as fh: | ||||||
|                 buf = fh.read() |                 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 |                 server = False | ||||||
|                 extension = cert.extensions.get_extension_for_oid(ExtensionOID.EXTENDED_KEY_USAGE) |                 for extension in cert["tbs_certificate"]["extensions"]: | ||||||
|                 for usage in extension.value: |                     if extension["extn_id"].native == u"extended_key_usage": | ||||||
|                     if usage == ExtendedKeyUsageOID.SERVER_AUTH: # TODO: IKE intermediate? |                         if u"server_auth" in extension["extn_value"].native: | ||||||
|                         server = True |                             server = True | ||||||
|                 yield common_name, path, buf, cert, server |                 yield common_name, path, buf, cert, server | ||||||
|  |  | ||||||
| def list_signed(): | def list_signed(): | ||||||
| @@ -203,10 +200,13 @@ def list_revoked(): | |||||||
| def list_server_names(): | def list_server_names(): | ||||||
|     return [cn for cn, path, buf, cert, server in list_signed() if server] |     return [cn for cn, path, buf, cert, server in list_signed() if server] | ||||||
|  |  | ||||||
| def export_crl(): | def export_crl(pem=True): | ||||||
|     sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) |     builder = CertificateListBuilder( | ||||||
|     sock.connect(const.SIGNER_SOCKET_PATH) |         config.AUTHORITY_CRL_URL, | ||||||
|     sock.send(b"export-crl\n") |         certificate, | ||||||
|  |         1 # TODO: monotonically increasing | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     for filename in os.listdir(config.REVOKED_DIR): |     for filename in os.listdir(config.REVOKED_DIR): | ||||||
|         if not filename.endswith(".pem"): |         if not filename.endswith(".pem"): | ||||||
|             continue |             continue | ||||||
| @@ -215,9 +215,15 @@ def export_crl(): | |||||||
|         revoked_path = os.path.join(config.REVOKED_DIR, filename) |         revoked_path = os.path.join(config.REVOKED_DIR, filename) | ||||||
|         # TODO: Skip expired certificates |         # TODO: Skip expired certificates | ||||||
|         s = os.stat(revoked_path) |         s = os.stat(revoked_path) | ||||||
|         sock.send(("%s:%d\n" % (serial_number, s.st_ctime)).encode("ascii")) |         builder.add_certificate( | ||||||
|     sock.sendall(b"\n") |             int(filename[:-4], 16), | ||||||
|     return sock.recv(32*1024*1024) |             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): | def delete_request(common_name): | ||||||
| @@ -236,88 +242,17 @@ def delete_request(common_name): | |||||||
|         config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest(), |         config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest(), | ||||||
|         headers={"User-Agent": "Certidude API"}) |         headers={"User-Agent": "Certidude API"}) | ||||||
|  |  | ||||||
| def 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): | 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") |     req_path = os.path.join(config.REQUESTS_DIR, common_name + ".pem") | ||||||
|     with open(req_path) as fh: |     with open(req_path) as fh: | ||||||
|         csr_buf = fh.read() |         csr_buf = fh.read() | ||||||
|         csr = x509.load_pem_x509_csr(csr_buf, backend=default_backend()) |         header, _, der_bytes = pem.unarmor(csr_buf) | ||||||
|     common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME) |         csr = CertificationRequest.load(der_bytes) | ||||||
|  |  | ||||||
|  |  | ||||||
|     # Sign with function below |     # Sign with function below | ||||||
|     cert, buf = _sign(csr, csr_buf, overwrite) |     cert, buf = _sign(csr, csr_buf, overwrite) | ||||||
| @@ -326,15 +261,17 @@ def sign(common_name, overwrite=False): | |||||||
|     return cert, buf |     return cert, buf | ||||||
|  |  | ||||||
| def _sign(csr, buf, overwrite=False): | def _sign(csr, buf, overwrite=False): | ||||||
|     assert buf.startswith("-----BEGIN CERTIFICATE REQUEST-----\n") |     # TODO: CRLDistributionPoints, OCSP URL, Certificate URL | ||||||
|     assert isinstance(csr, x509.CertificateSigningRequest) |  | ||||||
|  |  | ||||||
|     common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME) |     assert buf.startswith("-----BEGIN CERTIFICATE REQUEST-----\n") | ||||||
|     cert_path = os.path.join(config.SIGNED_DIR, "%s.pem" % common_name.value) |     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 |     renew = False | ||||||
|  |  | ||||||
|     attachments = [ |     attachments = [ | ||||||
|         (buf, "application/x-pem-file", common_name.value + ".csr"), |         (buf, "application/x-pem-file", common_name + ".csr"), | ||||||
|     ] |     ] | ||||||
|  |  | ||||||
|     revoked_path = None |     revoked_path = None | ||||||
| @@ -344,13 +281,18 @@ def _sign(csr, buf, overwrite=False): | |||||||
|     if os.path.exists(cert_path): |     if os.path.exists(cert_path): | ||||||
|         with open(cert_path) as fh: |         with open(cert_path) as fh: | ||||||
|             prev_buf = fh.read() |             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? |             # 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: |         if overwrite: | ||||||
|             # TODO: is this the best approach? |             # 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) |             revoked_path = os.path.join(config.REVOKED_DIR, "%s.pem" % prev_serial_hex) | ||||||
|             os.rename(cert_path, revoked_path) |             os.rename(cert_path, revoked_path) | ||||||
|             attachments += [(prev_buf, "application/x-pem-file", "deprecated.crt" if renew else "overwritten.crt")] |             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") |             raise EnvironmentError("Will not overwrite existing certificate") | ||||||
|  |  | ||||||
|     # Sign via signer process |     # Sign via signer process | ||||||
|     cert_buf = signer_exec("sign-request", buf) |     builder = CertificateBuilder({u'common_name': common_name }, csr_pubkey) | ||||||
|     cert = x509.load_pem_x509_certificate(cert_buf, default_backend()) |     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: |     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) |     os.rename(cert_path + ".part", cert_path) | ||||||
|     attachments.append((cert_buf, "application/x-pem-file", common_name.value + ".crt")) |     attachments.append((end_entity_cert_buf, "application/x-pem-file", common_name + ".crt")) | ||||||
|     cert_serial_hex = "%x" % cert.serial |     cert_serial_hex = "%x" % end_entity_cert.serial_number | ||||||
|  |  | ||||||
|     # Create symlink |     # Create symlink | ||||||
|     os.symlink( |     link_name = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % end_entity_cert.serial_number) | ||||||
|         "../%s.pem" % common_name.value, |     assert not os.path.exists(link_name), "Certificate with same serial number already exists: %s" % link_name | ||||||
|         os.path.join(config.SIGNED_BY_SERIAL_DIR, "%x.pem" % cert.serial)) |     os.symlink("../%s.pem" % common_name, link_name) | ||||||
|  |  | ||||||
|     # Copy filesystem attributes to newly signed certificate |     # Copy filesystem attributes to newly signed certificate | ||||||
|     if revoked_path: |     if revoked_path: | ||||||
| @@ -387,8 +351,8 @@ def _sign(csr, buf, overwrite=False): | |||||||
|  |  | ||||||
|     url = config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest() |     url = config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest() | ||||||
|     click.echo("Publishing certificate at %s ..." % url) |     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"}) |         headers={"User-Agent": "Certidude API", "Content-Type": "application/x-x509-user-cert"}) | ||||||
|  |  | ||||||
|     push.publish("request-signed", common_name.value) |     push.publish("request-signed", common_name) | ||||||
|     return cert, cert_buf |     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 string | ||||||
| import subprocess | import subprocess | ||||||
| import sys | import sys | ||||||
|  | from asn1crypto.util import timezone | ||||||
| from base64 import b64encode | from base64 import b64encode | ||||||
| from configparser import ConfigParser, NoOptionError, NoSectionError | from configparser import ConfigParser, NoOptionError, NoSectionError | ||||||
| from certidude.common import ip_address, ip_network, apt, rpm, pip, drop_privileges, selinux_fixup | 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 | # keyUsage, extendedKeyUsage - https://www.openssl.org/docs/apps/x509v3_client_config.html | ||||||
| # strongSwan key paths - https://wiki.strongswan.org/projects/1/wiki/SimpleCA | # strongSwan key paths - https://wiki.strongswan.org/projects/1/wiki/SimpleCA | ||||||
|  |  | ||||||
| # Parse command-line argument defaults from environment | NOW = datetime.utcnow() | ||||||
|  |  | ||||||
| NOW = datetime.utcnow().replace(tzinfo=None) |  | ||||||
|  |  | ||||||
| def fqdn_required(func): | def fqdn_required(func): | ||||||
|     def wrapped(**args): |     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: |             with open(certificate_path, "rb") as ch, open(request_path, "rb") as rh, open(key_path, "rb") as kh: | ||||||
|                 cert_buf = ch.read() |                 cert_buf = ch.read() | ||||||
|                 cert = asymmetric.load_certificate(cert_buf) |                 cert = asymmetric.load_certificate(cert_buf) | ||||||
|                 expires = cert.asn1["tbs_certificate"]["validity"]["not_after"].native |                 expires = cert.asn1["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None) | ||||||
|                 if renewal_overlap and datetime.now() > expires - timedelta(days=renewal_overlap): |                 if renewal_overlap and NOW > expires - timedelta(days=renewal_overlap): | ||||||
|                     click.echo("Certificate will expire %s, will attempt to renew" % expires) |                     click.echo("Certificate will expire %s, will attempt to renew" % expires) | ||||||
|                     renew = True |                     renew = True | ||||||
|                 headers["X-Renewal-Signature"] = b64encode( |                 headers["X-Renewal-Signature"] = b64encode( | ||||||
| @@ -931,15 +930,13 @@ def certidude_setup_openvpn_networkmanager(authority, remote, common_name, **pat | |||||||
| @fqdn_required | @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): | 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 |     # Install only rarely changing stuff from OS package management | ||||||
|     apt("python-setproctitle cython python-dev libkrb5-dev libffi-dev libssl-dev") |     apt("cython python-dev python-mimeparse python-markdown python-xattr python-jinja2 python-cffi python-ldap software-properties-common libsasl2-modules-gssapi-mit") | ||||||
|     apt("python-mimeparse python-markdown python-xattr python-jinja2 python-cffi") |     pip("gssapi falcon humanize ipaddress simplepam humanize requests") | ||||||
|     apt("python-ldap software-properties-common libsasl2-modules-gssapi-mit") |  | ||||||
|     pip("gssapi falcon humanize ipaddress simplepam humanize requests pyopenssl") |  | ||||||
|     click.echo("Software dependencies installed") |     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"): |     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") |         os.system("apt-get install -y libnginx-mod-nchan") | ||||||
|     if not os.path.exists("/usr/sbin/nginx"): |     if not os.path.exists("/usr/sbin/nginx"): | ||||||
|         os.system("apt-get install -y 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( |         builder.serial_number = random.randint( | ||||||
|             0x100000000000000000000000000000000000000, |             0x100000000000000000000000000000000000000, | ||||||
|             0xfffffffffffffffffffffffffffffffffffffff) |             0xfffffffffffffffffffffffffffffffffffffff) | ||||||
|         now = datetime.utcnow() |  | ||||||
|         builder.begin_date = now - timedelta(minutes=5) |         builder.begin_date = NOW - timedelta(minutes=5) | ||||||
|         builder.end_date = now + timedelta(days=authority_lifetime) |         builder.end_date = NOW + timedelta(days=authority_lifetime) | ||||||
|  |  | ||||||
|         if server_flags: |         if server_flags: | ||||||
|             builder.key_usage(set(['digital_signature', 'key_encipherment', 'key_cert_sign', 'crl_sign'])) |             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.extended_key_usage = set(['server_auth', "1.3.6.1.5.5.8.2.2"]) | ||||||
|  |  | ||||||
|         certificate = builder.build(private_key) |         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("sha1sum: %s" % hashlib.sha1(buf).hexdigest()) | ||||||
|             click.echo("sha256sum: %s" % hashlib.sha256(buf).hexdigest()) |             click.echo("sha256sum: %s" % hashlib.sha256(buf).hexdigest()) | ||||||
|         click.echo() |         click.echo() | ||||||
|         for ext in cert.extensions: |  | ||||||
|             print " -", ext.value |  | ||||||
|         click.echo() |  | ||||||
|  |  | ||||||
|     if not hide_requests: |     if not hide_requests: | ||||||
|         for common_name, path, buf, csr, server in authority.list_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: |             if not verbose: | ||||||
|                 click.echo("s " + path) |                 click.echo("s " + path) | ||||||
|                 continue |                 continue | ||||||
|  |             click.echo() | ||||||
|             click.echo(click.style(common_name, fg="blue")) |             click.echo(click.style(common_name, fg="blue")) | ||||||
|             click.echo("=" * len(common_name)) |             click.echo("=" * len(common_name)) | ||||||
|             click.echo("State: ? " + click.style("submitted", fg="yellow") + " " + naturaltime(created) + click.style(", %s" %created,  fg="white")) |             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: |     if show_signed: | ||||||
|         for common_name, path, buf, cert, server in authority.list_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 not verbose: | ||||||
|                 if cert.not_valid_before < NOW and cert.not_valid_after > NOW: |                 if signed < NOW and NOW < expires: | ||||||
|                     click.echo("v " + path) |                     click.echo("v " + path) | ||||||
|                 elif NOW > cert.not_valid_after: |                 elif expires < NOW: | ||||||
|                     click.echo("e " + path) |                     click.echo("e " + path) | ||||||
|                 else: |                 else: | ||||||
|                     click.echo("y " + path) |                     click.echo("y " + path) | ||||||
|                 continue |                 continue | ||||||
|  |             click.echo() | ||||||
|             click.echo(click.style(common_name, fg="blue") + " " + click.style("%x" % cert.serial, fg="white")) |             click.echo(click.style(common_name, fg="blue") + " " + click.style("%x" % cert.serial_number, fg="white")) | ||||||
|             click.echo("="*(len(common_name)+60)) |             click.echo("="*(len(common_name)+60)) | ||||||
|             expires = 0 # TODO |  | ||||||
|             if cert.not_valid_before < NOW and cert.not_valid_after > NOW: |             if signed < NOW and NOW < expires: | ||||||
|                 click.echo("Status: " + click.style("valid", fg="green") + " until " + naturaltime(cert.not_valid_after) + click.style(", %s" % cert.not_valid_after,  fg="white")) |                 click.echo("Status: " + click.style("valid", fg="green") + " until " + naturaltime(expires) + click.style(", %s" % expires,  fg="white")) | ||||||
|             elif NOW > cert.not_valid_after: |             elif NOW > expires: | ||||||
|                 click.echo("Status: " + click.style("expired", fg="red") + " " + naturaltime(expires) + click.style(", %s" %expires,  fg="white")) |                 click.echo("Status: " + click.style("expired", fg="red") + " " + naturaltime(expires) + click.style(", %s" % expires,  fg="white")) | ||||||
|             else: |             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() | ||||||
|             click.echo("openssl x509 -in %s -text -noout" % path) |             click.echo("openssl x509 -in %s -text -noout" % path) | ||||||
|             dump_common(common_name, path, cert) |             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: |     if show_revoked: | ||||||
|         for common_name, path, buf, cert, server in authority.list_revoked(): |         for common_name, path, buf, cert, server in authority.list_revoked(): | ||||||
|             if not verbose: |             if not verbose: | ||||||
|                 click.echo("r " + path) |                 click.echo("r " + path) | ||||||
|                 continue |                 continue | ||||||
|  |             click.echo() | ||||||
|             click.echo(click.style(common_name, fg="blue") + " " + click.style("%x" % cert.serial, fg="white")) |             click.echo(click.style(common_name, fg="blue") + " " + click.style("%x" % cert.serial_number, fg="white")) | ||||||
|             click.echo("="*(len(common_name)+60)) |             click.echo("="*(len(common_name)+60)) | ||||||
|  |  | ||||||
|             _, _, _, _, _, _, _, _, mtime, _ = os.stat(path) |             _, _, _, _, _, _, _, _, 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("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) |             click.echo("openssl x509 -in %s -text -noout" % path) | ||||||
|             dump_common(common_name, path, cert) |             dump_common(common_name, path, cert) | ||||||
|  |             for ext in cert["tbs_certificate"]["extensions"]: | ||||||
|     click.echo() |                 print " - %s: %s" % (ext["extn_id"].native, repr(ext["extn_value"].native)) | ||||||
|  |  | ||||||
|  |  | ||||||
| @click.command("sign", help="Sign certificate") | @click.command("sign", help="Sign certificate") | ||||||
| @click.argument("common_name") | @click.argument("common_name") | ||||||
| @click.option("--overwrite", "-o", default=False, is_flag=True, help="Revoke valid certificate with same CN") | @click.option("--overwrite", "-o", default=False, is_flag=True, help="Revoke valid certificate with same CN") | ||||||
| def certidude_sign(common_name, overwrite): | def certidude_sign(common_name, overwrite): | ||||||
|     drop_privileges() |  | ||||||
|     from certidude import authority |     from certidude import authority | ||||||
|  |     drop_privileges() | ||||||
|     cert = authority.sign(common_name, overwrite) |     cert = authority.sign(common_name, overwrite) | ||||||
|  |  | ||||||
|  |  | ||||||
| @click.command("revoke", help="Revoke certificate") | @click.command("revoke", help="Revoke certificate") | ||||||
| @click.argument("common_name") | @click.argument("common_name") | ||||||
| def certidude_revoke(common_name): | def certidude_revoke(common_name): | ||||||
|     drop_privileges() |  | ||||||
|     from certidude import authority |     from certidude import authority | ||||||
|  |     drop_privileges() | ||||||
|     authority.revoke(common_name) |     authority.revoke(common_name) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -1242,10 +1241,10 @@ def certidude_revoke(common_name): | |||||||
| def certidude_cron(): | def certidude_cron(): | ||||||
|     import itertools |     import itertools | ||||||
|     from certidude import authority, config |     from certidude import authority, config | ||||||
|     now = datetime.now() |  | ||||||
|     for cn, path, buf, cert, server in itertools.chain(authority.list_signed(), authority.list_revoked()): |     for cn, path, buf, cert, server in itertools.chain(authority.list_signed(), authority.list_revoked()): | ||||||
|         if cert.not_valid_after < now: |         expires = cert["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None) | ||||||
|             expired_path = os.path.join(config.EXPIRED_DIR, "%x.pem" % cert.serial) |         if expires < NOW: | ||||||
|  |             expired_path = os.path.join(config.EXPIRED_DIR, "%x.pem" % cert.serial_number) | ||||||
|             assert not os.path.exists(expired_path) |             assert not os.path.exists(expired_path) | ||||||
|             os.rename(path, expired_path) |             os.rename(path, expired_path) | ||||||
|             click.echo("Moved %s to %s" % (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): | def certidude_serve(port, listen, fork): | ||||||
|     import pwd |     import pwd | ||||||
|     from setproctitle import setproctitle |     from setproctitle import setproctitle | ||||||
|     from certidude.signer import SignServer |  | ||||||
|     from certidude import authority, const, push |     from certidude import authority, const, push | ||||||
|  |  | ||||||
|     if port == 80: |     if port == 80: | ||||||
| @@ -1272,7 +1270,7 @@ def certidude_serve(port, listen, fork): | |||||||
|  |  | ||||||
|     # Rebuild reverse mapping |     # Rebuild reverse mapping | ||||||
|     for cn, path, buf, cert, server in authority.list_signed(): |     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): |         if not os.path.exists(by_serial): | ||||||
|             click.echo("Linking %s to ../%s.pem" % (by_serial, cn)) |             click.echo("Linking %s to ../%s.pem" % (by_serial, cn)) | ||||||
|             os.symlink("../%s.pem" % cn, by_serial) |             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")) |     rh.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")) | ||||||
|     log_handlers.append(rh) |     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" % |     click.echo("Users subnets: %s" % | ||||||
|         ", ".join([str(j) for j in config.USER_SUBNETS])) |         ", ".join([str(j) for j in config.USER_SUBNETS])) | ||||||
|     click.echo("Administrative subnets: %s" % |     click.echo("Administrative subnets: %s" % | ||||||
| @@ -1392,7 +1341,6 @@ def certidude_serve(port, listen, fork): | |||||||
|         def cleanup_handler(*args): |         def cleanup_handler(*args): | ||||||
|             push.publish("server-stopped") |             push.publish("server-stopped") | ||||||
|             logger.debug(u"Shutting down Certidude") |             logger.debug(u"Shutting down Certidude") | ||||||
|             assert authority.signer_exec("exit") == "ok" |  | ||||||
|             sys.exit(0) # TODO: use another code, needs test refactor |             sys.exit(0) # TODO: use another code, needs test refactor | ||||||
|  |  | ||||||
|         import signal |         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") | SERVICES_CONFIG_PATH = os.path.join(CONFIG_DIR, "services.conf") | ||||||
| SERVER_PID_PATH = os.path.join(RUN_DIR, "server.pid") | SERVER_PID_PATH = os.path.join(RUN_DIR, "server.pid") | ||||||
| SERVER_LOG_PATH = "/var/log/certidude-server.log" | 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/" | STORAGE_PATH = "/var/lib/certidude/" | ||||||
|  |  | ||||||
| try: | 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"> | <li id="request-{{ request.common_name | replace('@', '--') | replace('.', '-') }}" class="filterable"> | ||||||
|  |  | ||||||
| <a class="button icon download" href="/api/request/{{request.common_name}}/">Fetch</a> | <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> | <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 }}. | 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 | The new certificate is valid from {{ builder.begin_date }} until | ||||||
| {{ cert.not_valid_after }}. | {{ builder.end_date }}. | ||||||
|  |  | ||||||
| Services making use of those certificates should continue working as expected. | 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 }} | with serial number {{ cert_serial_hex }} | ||||||
| was signed{% if signer %} by {{ signer }}{% endif %}. | was signed{% if signer %} by {{ signer }}{% endif %}. | ||||||
|  |  | ||||||
| The certificate is valid from {{ cert.not_valid_before }} until | The certificate is valid from {{ builder.begin_date }} until | ||||||
| {{ cert.not_valid_after }}. | {{ builder.end_date }}. | ||||||
|  |  | ||||||
| {% if overwritten %} | {% if overwritten %} | ||||||
| By doing so existing certificate with the same common name | By doing so existing certificate with the same common name | ||||||
|   | |||||||
| @@ -1,69 +1,136 @@ | |||||||
| # To set up SSL certificates using Let's Encrypt run: | # 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 | # 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_req_zone $binary_remote_addr  zone=api:10m rate=30r/m; | ||||||
| limit_conn_zone $binary_remote_addr zone=addr:10m; | 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 { | 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 }}; |     server_name {{ common_name }}; | ||||||
|     listen 80 default_server; |     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/ { |     location /api/ { | ||||||
|         proxy_pass http://127.0.1.1:8080/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; |         limit_req zone=api burst=5; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     # This is for Let's Encrypt |     # Path to static files | ||||||
|     location /.well-known/ { |     root {{static_path}}; | ||||||
|         alias /var/www/html/.well-known/; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     # Rewrite /cgi-bin/pkiclient.exe to /api/scep for SCEP protocol |     # Rewrite /cgi-bin/pkiclient.exe to /api/scep for SCEP protocol | ||||||
|     location /cgi-bin/pkiclient.exe { |     location /cgi-bin/pkiclient.exe { | ||||||
|         rewrite /cgi-bin/pkiclient.exe /api/scep/ last; |         rewrite /cgi-bin/pkiclient.exe /api/scep/ last; | ||||||
|     } |     } | ||||||
|  |  | ||||||
| {% if not push_server %} |     # Long poll for CSR submission | ||||||
|     # 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/ |  | ||||||
|  |  | ||||||
|     location ~ "^/lp/sub/(.*)" { |     location ~ "^/lp/sub/(.*)" { | ||||||
|         nchan_channel_id $1; |         nchan_channel_id $1; | ||||||
|         nchan_subscriber longpoll; |         nchan_subscriber longpoll; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     # Comment everything below in this server definition if you're using HTTPS | ||||||
|  |  | ||||||
|  |     # Event source for web interface | ||||||
|     location ~ "^/ev/sub/(.*)" { |     location ~ "^/ev/sub/(.*)" { | ||||||
|         nchan_channel_id $1; |         nchan_channel_id $1; | ||||||
|         nchan_subscriber eventsource; |         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 %} | {% if not push_server %} | ||||||
| @@ -75,13 +142,11 @@ server { | |||||||
|     location ~ "^/lp/pub/(.*)" { |     location ~ "^/lp/pub/(.*)" { | ||||||
|         nchan_publisher; |         nchan_publisher; | ||||||
|         nchan_channel_id $1; |         nchan_channel_id $1; | ||||||
|         nchan_message_buffer_length 0; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     location ~ "^/ev/pub/(.*)" { |     location ~ "^/ev/pub/(.*)" { | ||||||
|         nchan_publisher; |         nchan_publisher; | ||||||
|         nchan_channel_id $1; |         nchan_channel_id $1; | ||||||
|         nchan_message_buffer_length 0; |  | ||||||
|     } |     } | ||||||
| } | } | ||||||
| {% endif %} | {% endif %} | ||||||
|   | |||||||
| @@ -1,3 +1,5 @@ | |||||||
| click>=6.7 | click>=6.7 | ||||||
| configparser>=3.5.0 | configparser>=3.5.0 | ||||||
| certbuilder | certbuilder | ||||||
|  | crlbuilder | ||||||
|  | oscrypto | ||||||
|   | |||||||
| @@ -80,12 +80,6 @@ def clean_client(): | |||||||
|  |  | ||||||
|  |  | ||||||
| def clean_server(): | 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"): |     if os.path.exists("/run/certidude/server.pid"): | ||||||
|         with open("/run/certidude/server.pid") as fh: |         with open("/run/certidude/server.pid") as fh: | ||||||
|             try: |             try: | ||||||
| @@ -239,16 +233,18 @@ def test_cli_setup_authority(): | |||||||
|     os.setgid(0) # Restore GID |     os.setgid(0) # Restore GID | ||||||
|     os.umask(0022) |     os.umask(0022) | ||||||
|  |  | ||||||
|  |     # Make sure nginx is running | ||||||
|     assert not result.exception, result.output |     assert not result.exception, result.output | ||||||
|     assert os.getuid() == 0 and os.getgid() == 0, "Serve dropped permissions incorrectly!" |     assert os.getuid() == 0 and os.getgid() == 0, "Serve dropped permissions incorrectly!" | ||||||
|     assert os.system("nginx -t") == 0, "invalid nginx configuration" |     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" |     assert os.path.exists("/run/nginx.pid"), "nginx wasn't started up properly" | ||||||
|  |  | ||||||
|     from certidude import config, authority, auth, user |     from certidude import config, authority, auth, user | ||||||
|     assert authority.ca_cert.serial_number >= 0x100000000000000000000000000000000000000 |     assert authority.certificate.serial_number >= 0x100000000000000000000000000000000000000 | ||||||
|     assert authority.ca_cert.serial_number <= 0xfffffffffffffffffffffffffffffffffffffff |     assert authority.certificate.serial_number <= 0xfffffffffffffffffffffffffffffffffffffff | ||||||
|     assert authority.ca_cert.not_valid_before < datetime.now() |     assert authority.certificate["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None) < datetime.utcnow() | ||||||
|     assert authority.ca_cert.not_valid_after > datetime.now() + timedelta(days=7000) |     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("lauri@fedora-123") == False | ||||||
|     assert authority.server_flags("fedora-123") == False |     assert authority.server_flags("fedora-123") == False | ||||||
|     assert authority.server_flags("vpn.example.lan") == True |     assert authority.server_flags("vpn.example.lan") == True | ||||||
| @@ -412,12 +408,12 @@ def test_cli_setup_authority(): | |||||||
|     assert not result.exception, result.output |     assert not result.exception, result.output | ||||||
|  |  | ||||||
|     # Test sign API call |     # 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 |     assert r.status_code == 401, r.text | ||||||
|     r = client().simulate_patch("/api/request/test/", |     r = client().simulate_post("/api/request/test/", | ||||||
|         headers={"Authorization":usertoken}) |         headers={"Authorization":usertoken}) | ||||||
|     assert r.status_code == 403, r.text |     assert r.status_code == 403, r.text | ||||||
|     r = client().simulate_patch("/api/request/test/", |     r = client().simulate_post("/api/request/test/", | ||||||
|         headers={"Authorization":admintoken}) |         headers={"Authorization":admintoken}) | ||||||
|     assert r.status_code == 201, r.text |     assert r.status_code == 201, r.text | ||||||
|     assert "Signed " in inbox.pop(), inbox |     assert "Signed " in inbox.pop(), inbox | ||||||
| @@ -476,7 +472,7 @@ def test_cli_setup_authority(): | |||||||
|     # Test revocations API call |     # Test revocations API call | ||||||
|     r = client().simulate_get("/api/revoked/", |     r = client().simulate_get("/api/revoked/", | ||||||
|         headers={"Accept":"application/x-pem-file"}) |         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" |     assert r.headers.get('content-type') == "application/x-pem-file" | ||||||
|  |  | ||||||
|     r = client().simulate_get("/api/revoked/") |     r = client().simulate_get("/api/revoked/") | ||||||
| @@ -672,29 +668,11 @@ def test_cli_setup_authority(): | |||||||
|     assert "/ev/sub/" in r.text, r.text |     assert "/ev/sub/" in r.text, r.text | ||||||
|     assert r.json, r.text |     assert r.json, r.text | ||||||
|     assert r.json.get("authority"), r.text |     assert r.json.get("authority"), r.text | ||||||
|     assert r.json.get("authority").get("events"), r.text |     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 | ||||||
|     ### Subscribe to event source ### |     assert ev_url.startswith("http://ca.example.lan/ev/sub/") | ||||||
|     ################################# |  | ||||||
|  |  | ||||||
|     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 |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     ####################### |     ####################### | ||||||
| @@ -704,6 +682,7 @@ def test_cli_setup_authority(): | |||||||
|     r = client().simulate_post("/api/token/") |     r = client().simulate_post("/api/token/") | ||||||
|     assert r.status_code == 404, r.text |     assert r.status_code == 404, r.text | ||||||
|  |  | ||||||
|  |     """ | ||||||
|     config.BUNDLE_FORMAT = "ovpn" |     config.BUNDLE_FORMAT = "ovpn" | ||||||
|     config.USER_ENROLLMENT_ALLOWED = True |     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.status_code == 200 # token consumed by anyone on unknown device | ||||||
|     assert r2.headers.get('content-type') == "application/x-pkcs12" |     assert r2.headers.get('content-type') == "application/x-pkcs12" | ||||||
|     assert "Signed " in inbox.pop(), inbox |     assert "Signed " in inbox.pop(), inbox | ||||||
|  |     """ | ||||||
|  |  | ||||||
|     # Beyond this point don't use client() |     # Beyond this point don't use client() | ||||||
|     const.STORAGE_PATH = "/tmp/" |     const.STORAGE_PATH = "/tmp/" | ||||||
| @@ -765,12 +745,13 @@ def test_cli_setup_authority(): | |||||||
|     result = runner.invoke(cli, ["request", "--no-wait"]) |     result = runner.invoke(cli, ["request", "--no-wait"]) | ||||||
|     assert not result.exception, result.output |     assert not result.exception, result.output | ||||||
|     assert not os.path.exists("/run/certidude/ca.example.lan.pid"), 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() |     child_pid = os.fork() | ||||||
|     if not child_pid: |     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 not result.exception, result.output | ||||||
|  |         assert "Publishing request-signed event 'www.example.lan' on http://localhost/ev/pub/" in result.output, result.output | ||||||
|         return |         return | ||||||
|     else: |     else: | ||||||
|         os.waitpid(child_pid, 0) |         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 not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output | ||||||
|     #assert "Writing certificate to:" in result.output, result.output |     #assert "Writing certificate to:" in result.output, result.output | ||||||
|     assert "Attached renewal signature" 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 |     # Test nginx setup | ||||||
|     assert os.system("nginx -t") == 0, "Generated nginx config was invalid" |     assert os.system("nginx -t") == 0, "Generated nginx config was invalid" | ||||||
|  |  | ||||||
|     # TODO: test client verification with curl |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     ############### |     ############### | ||||||
|     ### OpenVPN ### |     ### OpenVPN ### | ||||||
| @@ -818,13 +796,17 @@ def test_cli_setup_authority(): | |||||||
|  |  | ||||||
|     result = runner.invoke(cli, ["request", "--no-wait"]) |     result = runner.invoke(cli, ["request", "--no-wait"]) | ||||||
|     assert not result.exception, result.output |     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("/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() |     child_pid = os.fork() | ||||||
|     if not child_pid: |     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 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 |         return | ||||||
|     else: |     else: | ||||||
|         os.waitpid(child_pid, 0) |         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: Check that tunnel interfaces came up, perhaps try to ping? | ||||||
|     # TODO: assert key, req, cert paths were included correctly in OpenVPN config |     # 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 ### |     ### IPSec ### | ||||||
|     ############### |     ############# | ||||||
|  |  | ||||||
|  |     # Setup gateway | ||||||
|  |  | ||||||
|     clean_client() |     clean_client() | ||||||
|  |  | ||||||
| @@ -882,11 +1018,15 @@ def test_cli_setup_authority(): | |||||||
|     result = runner.invoke(cli, ["request", "--no-wait"]) |     result = runner.invoke(cli, ["request", "--no-wait"]) | ||||||
|     assert not result.exception, result.output |     assert not result.exception, result.output | ||||||
|     assert not os.path.exists("/run/certidude/ca.example.lan.pid"), 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() |     child_pid = os.fork() | ||||||
|     if not child_pid: |     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 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 |         return | ||||||
|     else: |     else: | ||||||
|         os.waitpid(child_pid, 0) |         os.waitpid(child_pid, 0) | ||||||
| @@ -898,7 +1038,8 @@ def test_cli_setup_authority(): | |||||||
|     assert "Writing certificate to:" in result.output, result.output |     assert "Writing certificate to:" in result.output, result.output | ||||||
|     assert os.path.exists("/tmp/ca.example.lan/server_cert.pem") |     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/client.conf") | ||||||
|     os.unlink("/etc/certidude/services.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 |     assert "Writing certificate to:" in result.output, result.output | ||||||
|  |  | ||||||
|  |     # IPSec using NetworkManager | ||||||
|     ###################### |  | ||||||
|     ### 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 |  | ||||||
|  |  | ||||||
|     clean_client() |     clean_client() | ||||||
|  |  | ||||||
| @@ -1158,11 +1283,15 @@ def test_cli_setup_authority(): | |||||||
|     ################## |     ################## | ||||||
|  |  | ||||||
|     os.umask(0022) |     os.umask(0022) | ||||||
|     assert not os.system("git clone  https://github.com/certnanny/sscep /tmp/sscep") |     if not os.path.exists("/tmp/sscep"): | ||||||
|     assert not os.system("cd /tmp/sscep && ./Configure && make sscep_dyn") |         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("/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") |     if not os.path.exists("/tmp/key.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("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") |     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 |     # TODO: test e-mails at this point | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user