pax_global_header00006660000000000000000000000064140403134030014502gustar00rootroot0000000000000052 comment=40c98e8d7c8914b1571d406140c4110a0f70c760 certipy-0.1.3/000077500000000000000000000000001404031340300131625ustar00rootroot00000000000000certipy-0.1.3/PKG-INFO000066400000000000000000000110441404031340300142570ustar00rootroot00000000000000Metadata-Version: 2.1 Name: certipy Version: 0.1.3 Summary: Utility to create and sign CAs and certificates Home-page: https://github.com/LLNL/certipy Author: Thomas Mendoza Author-email: mendoza33@llnl.gov License: BSD Description: Certipy ======= A simple python tool for creating certificate authorities and certificates on the fly. Introduction ------------ Certipy was made to simplify the certificate creation process. To that end, Certipy exposes methods for creating and managing certificate authorities, certificates, signing and building trust bundles. Behind the scenes Certipy: - Manages records of all certificates it creates - External certs can be imported and managed by Certipy - Maintains signing hierarchy - Persists certificates to files with appropriate permissions Usage ----- Command line ~~~~~~~~~~~~ Creating a certificate authority: Certipy defaults to writing certs and certipy.json into a folder called ``out`` in your current directory. :: $ certipy foo FILES {'ca': '', 'cert': 'out/foo/foo.crt', 'key': 'out/foo/foo.key'} IS_CA True SERIAL 0 SIGNEES None PARENT_CA Creating and signing a key-cert pair: :: $ certipy bar --ca-name foo FILES {'ca': 'out/foo/foo.crt', 'key': 'out/bar/bar.key', 'cert': 'out/bar/bar.crt'} IS_CA False SERIAL 0 SIGNEES None PARENT_CA foo Removal: :: certipy --rm bar Deleted: FILES {'ca': 'out/foo/foo.crt', 'key': 'out/bar/bar.key', 'cert': 'out/bar/bar.crt'} IS_CA False SERIAL 0 SIGNEES None PARENT_CA foo Code ~~~~ Creating a certificate authority: :: from certipy import Certipy certipy = Certipy(store_dir='/tmp') certipy.create_ca('foo') record = certipy.store.get_record('foo') Creating and signing a key-cert pair: :: certipy.create_signed_pair('bar', 'foo') record = certipy.store.get_record('bar') Creating trust: :: certipy.create_ca_bundle('ca-bundle.crt') # or to trust specific certs only: certipy.create_ca_bundle_for_names('ca-bundle.crt', ['bar']) Removal: :: record = certipy.remove_files('bar') Records are dicts with the following structure: :: { 'serial': 0, 'is_ca': true, 'parent_ca': 'ca_name', 'signees': { 'signee_name': 1 }, 'files': { 'key': 'path/to/key.key', 'cert': 'path/to/cert.crt', 'ca': 'path/to/ca.crt', } } The ``signees`` will be empty for non-CA certificates. The ``signees`` field is stored as a python ``Counter``. These relationships are used to build trust bundles. Information in Certipy is generally passed around as records which point to actual files. For most ``_record`` methods, there are generally equivalent ``_file`` methods that operate on files themselves. The former will only affect records in Certipy’s store and the latter will affect both (something happens to the file, the record for it should change, too). Release ~~~~~~~ Certipy is released under BSD license. For more details see the LICENSE file. LLNL-CODE-754897 Keywords: pki ssl tls certificates Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: Topic :: Utilities Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Provides-Extra: test Provides-Extra: dev certipy-0.1.3/README.md000066400000000000000000000051561404031340300144500ustar00rootroot00000000000000# Certipy A simple python tool for creating certificate authorities and certificates on the fly. ## Introduction Certipy was made to simplify the certificate creation process. To that end, Certipy exposes methods for creating and managing certificate authorities, certificates, signing and building trust bundles. Behind the scenes Certipy: * Manages records of all certificates it creates * External certs can be imported and managed by Certipy * Maintains signing hierarchy * Persists certificates to files with appropriate permissions ## Usage ### Command line Creating a certificate authority: Certipy defaults to writing certs and certipy.json into a folder called `out` in your current directory. ``` $ certipy foo FILES {'ca': '', 'cert': 'out/foo/foo.crt', 'key': 'out/foo/foo.key'} IS_CA True SERIAL 0 SIGNEES None PARENT_CA ``` Creating and signing a key-cert pair: ``` $ certipy bar --ca-name foo FILES {'ca': 'out/foo/foo.crt', 'key': 'out/bar/bar.key', 'cert': 'out/bar/bar.crt'} IS_CA False SERIAL 0 SIGNEES None PARENT_CA foo ``` Removal: ``` certipy --rm bar Deleted: FILES {'ca': 'out/foo/foo.crt', 'key': 'out/bar/bar.key', 'cert': 'out/bar/bar.crt'} IS_CA False SERIAL 0 SIGNEES None PARENT_CA foo ``` ### Code Creating a certificate authority: ``` from certipy import Certipy certipy = Certipy(store_dir='/tmp') certipy.create_ca('foo') record = certipy.store.get_record('foo') ``` Creating and signing a key-cert pair: ``` certipy.create_signed_pair('bar', 'foo') record = certipy.store.get_record('bar') ``` Creating trust: ``` certipy.create_ca_bundle('ca-bundle.crt') # or to trust specific certs only: certipy.create_ca_bundle_for_names('ca-bundle.crt', ['bar']) ``` Removal: ``` record = certipy.remove_files('bar') ``` Records are dicts with the following structure: ``` { 'serial': 0, 'is_ca': true, 'parent_ca': 'ca_name', 'signees': { 'signee_name': 1 }, 'files': { 'key': 'path/to/key.key', 'cert': 'path/to/cert.crt', 'ca': 'path/to/ca.crt', } } ``` The `signees` will be empty for non-CA certificates. The `signees` field is stored as a python `Counter`. These relationships are used to build trust bundles. Information in Certipy is generally passed around as records which point to actual files. For most `_record` methods, there are generally equivalent `_file` methods that operate on files themselves. The former will only affect records in Certipy's store and the latter will affect both (something happens to the file, the record for it should change, too). ### Release Certipy is released under BSD license. For more details see the LICENSE file. LLNL-CODE-754897 certipy-0.1.3/certipy.egg-info/000077500000000000000000000000001404031340300163335ustar00rootroot00000000000000certipy-0.1.3/certipy.egg-info/PKG-INFO000066400000000000000000000110441404031340300174300ustar00rootroot00000000000000Metadata-Version: 2.1 Name: certipy Version: 0.1.3 Summary: Utility to create and sign CAs and certificates Home-page: https://github.com/LLNL/certipy Author: Thomas Mendoza Author-email: mendoza33@llnl.gov License: BSD Description: Certipy ======= A simple python tool for creating certificate authorities and certificates on the fly. Introduction ------------ Certipy was made to simplify the certificate creation process. To that end, Certipy exposes methods for creating and managing certificate authorities, certificates, signing and building trust bundles. Behind the scenes Certipy: - Manages records of all certificates it creates - External certs can be imported and managed by Certipy - Maintains signing hierarchy - Persists certificates to files with appropriate permissions Usage ----- Command line ~~~~~~~~~~~~ Creating a certificate authority: Certipy defaults to writing certs and certipy.json into a folder called ``out`` in your current directory. :: $ certipy foo FILES {'ca': '', 'cert': 'out/foo/foo.crt', 'key': 'out/foo/foo.key'} IS_CA True SERIAL 0 SIGNEES None PARENT_CA Creating and signing a key-cert pair: :: $ certipy bar --ca-name foo FILES {'ca': 'out/foo/foo.crt', 'key': 'out/bar/bar.key', 'cert': 'out/bar/bar.crt'} IS_CA False SERIAL 0 SIGNEES None PARENT_CA foo Removal: :: certipy --rm bar Deleted: FILES {'ca': 'out/foo/foo.crt', 'key': 'out/bar/bar.key', 'cert': 'out/bar/bar.crt'} IS_CA False SERIAL 0 SIGNEES None PARENT_CA foo Code ~~~~ Creating a certificate authority: :: from certipy import Certipy certipy = Certipy(store_dir='/tmp') certipy.create_ca('foo') record = certipy.store.get_record('foo') Creating and signing a key-cert pair: :: certipy.create_signed_pair('bar', 'foo') record = certipy.store.get_record('bar') Creating trust: :: certipy.create_ca_bundle('ca-bundle.crt') # or to trust specific certs only: certipy.create_ca_bundle_for_names('ca-bundle.crt', ['bar']) Removal: :: record = certipy.remove_files('bar') Records are dicts with the following structure: :: { 'serial': 0, 'is_ca': true, 'parent_ca': 'ca_name', 'signees': { 'signee_name': 1 }, 'files': { 'key': 'path/to/key.key', 'cert': 'path/to/cert.crt', 'ca': 'path/to/ca.crt', } } The ``signees`` will be empty for non-CA certificates. The ``signees`` field is stored as a python ``Counter``. These relationships are used to build trust bundles. Information in Certipy is generally passed around as records which point to actual files. For most ``_record`` methods, there are generally equivalent ``_file`` methods that operate on files themselves. The former will only affect records in Certipy’s store and the latter will affect both (something happens to the file, the record for it should change, too). Release ~~~~~~~ Certipy is released under BSD license. For more details see the LICENSE file. LLNL-CODE-754897 Keywords: pki ssl tls certificates Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: Topic :: Utilities Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Provides-Extra: test Provides-Extra: dev certipy-0.1.3/certipy.egg-info/SOURCES.txt000066400000000000000000000005031404031340300202150ustar00rootroot00000000000000README.md setup.py certipy/__init__.py certipy/certipy.py certipy/command_line.py certipy.egg-info/PKG-INFO certipy.egg-info/SOURCES.txt certipy.egg-info/dependency_links.txt certipy.egg-info/entry_points.txt certipy.egg-info/requires.txt certipy.egg-info/top_level.txt certipy/test/__init__.py certipy/test/test_certipy.pycertipy-0.1.3/certipy.egg-info/dependency_links.txt000066400000000000000000000000011404031340300224010ustar00rootroot00000000000000 certipy-0.1.3/certipy.egg-info/entry_points.txt000066400000000000000000000000671404031340300216340ustar00rootroot00000000000000[console_scripts] certipy = certipy.command_line:main certipy-0.1.3/certipy.egg-info/requires.txt000066400000000000000000000000471404031340300207340ustar00rootroot00000000000000pyopenssl [dev] pytest [test] pytest certipy-0.1.3/certipy.egg-info/top_level.txt000066400000000000000000000000101404031340300210540ustar00rootroot00000000000000certipy certipy-0.1.3/certipy/000077500000000000000000000000001404031340300146415ustar00rootroot00000000000000certipy-0.1.3/certipy/__init__.py000066400000000000000000000070231404031340300167540ustar00rootroot00000000000000############################################################################### # Copyright (c) 2018, Lawrence Livermore National Security, LLC # Produced at the Lawrence Livermore National Laboratory # Written by Thomas Mendoza mendoza33@llnl.gov # LLNL-CODE-754897 # All rights reserved # # This file is part of Certipy. For details, see # https://github.com/LLNL/certipy. Please also read this link - Additional # BSD Notice. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the disclaimer below. # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the disclaimer (as noted below) in # the documentation and/or other materials provided with the distribution. # * Neither the name of the LLNS/LLNL nor the names of its contributors may # be used to endorse or promote products derived from this software without # specific prior written permission. # # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL LAWRENCE LIVERMORE NATIONAL SECURITY, LLC, # THE U.S. DEPARTMENT OF ENERGY OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # # Additional BSD Notice # # 1. This notice is required to be provided under our contract with the U.S. # Department of Energy (DOE). This work was produced at Lawrence Livermore # National Laboratory under Contract No. DE-AC52-07NA27344 with the DOE. # # 2. Neither the United States Government nor Lawrence Livermore National # Security, LLC nor any of their employees, makes any warranty, express or # implied, or assumes any liability or responsibility for the accuracy, # completeness, or usefulness of any information, apparatus, product, # or process disclosed, or represents that its use would not infringe # privately-owned rights. # # 3. Also, reference herein to any specific commercial products, process, or # services by trade name, trademark, manufacturer or otherwise does not # necessarily constitute or imply its endorsement, recommendation, or favoring # by the United States Government or Lawrence Livermore National Security, LLC. # The views and opinions of authors expressed herein do not necessarily state # or reflect those of the United States Government or Lawrence Livermore # National Security, LLC, and shall not be used for advertising or product # endorsement purposes. ############################################################################### from certipy.certipy import ( TLSFileType, TLSFile, TLSFileBundle, CertStore, open_tls_file, CertNotFoundError, CertExistsError, CertificateAuthorityInUseError, Certipy ) __all__ = ( TLSFileType, TLSFile, TLSFileBundle, CertStore, open_tls_file, CertNotFoundError, CertExistsError, CertificateAuthorityInUseError, Certipy ) certipy-0.1.3/certipy/certipy.py000066400000000000000000000672141404031340300167040ustar00rootroot00000000000000############################################################################### # Copyright (c) 2018, Lawrence Livermore National Security, LLC # Produced at the Lawrence Livermore National Laboratory # Written by Thomas Mendoza mendoza33@llnl.gov # LLNL-CODE-754897 # All rights reserved # # This file is part of Certipy. For details, see # https://github.com/LLNL/certipy. Please also read this link - Additional # BSD Notice. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the disclaimer below. # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the disclaimer (as noted below) in # the documentation and/or other materials provided with the distribution. # * Neither the name of the LLNS/LLNL nor the names of its contributors may # be used to endorse or promote products derived from this software without # specific prior written permission. # # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL LAWRENCE LIVERMORE NATIONAL SECURITY, LLC, # THE U.S. DEPARTMENT OF ENERGY OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # # Additional BSD Notice # # 1. This notice is required to be provided under our contract with the U.S. # Department of Energy (DOE). This work was produced at Lawrence Livermore # National Laboratory under Contract No. DE-AC52-07NA27344 with the DOE. # # 2. Neither the United States Government nor Lawrence Livermore National # Security, LLC nor any of their employees, makes any warranty, express or # implied, or assumes any liability or responsibility for the accuracy, # completeness, or usefulness of any information, apparatus, product, # or process disclosed, or represents that its use would not infringe # privately-owned rights. # # 3. Also, reference herein to any specific commercial products, process, or # services by trade name, trademark, manufacturer or otherwise does not # necessarily constitute or imply its endorsement, recommendation, or favoring # by the United States Government or Lawrence Livermore National Security, LLC. # The views and opinions of authors expressed herein do not necessarily state # or reflect those of the United States Government or Lawrence Livermore # National Security, LLC, and shall not be used for advertising or product # endorsement purposes. ############################################################################### import os import json import argparse import logging import shutil from enum import Enum from collections import Counter from OpenSSL import crypto from contextlib import contextmanager class TLSFileType(Enum): KEY = 'key' CERT = 'cert' CA = 'ca' class CertNotFoundError(Exception): def __init__(self, message, errors=None): super().__init__(message) self.errors = errors class CertExistsError(Exception): def __init__(self, message, errors=None): super().__init__(message) self.errors = errors class CertificateAuthorityInUseError(Exception): def __init__(self, message, errors=None): super().__init__(message) self.errors = errors @contextmanager def open_tls_file(file_path, mode, private=True): """Context to ensure correct file permissions for certs and directories Ensures: - A containing directory with appropriate permissions - Correct file permissions based on what the file is (0o600 for keys and 0o644 for certs) """ containing_dir = os.path.dirname(file_path) fh = None try: if 'w' in mode: os.chmod(containing_dir, mode=0o755) fh = open(file_path, mode) except OSError as e: if 'w' in mode: os.makedirs(containing_dir, mode=0o755, exist_ok=True) os.chmod(containing_dir, mode=0o755) fh = open(file_path, 'w') else: raise yield fh mode = 0o600 if private else 0o644 os.chmod(file_path, mode=mode) fh.close() class TLSFile(): """Describes basic information about files used for TLS""" def __init__(self, file_path, encoding=crypto.FILETYPE_PEM, file_type=TLSFileType.CERT, x509=None): self.file_path = file_path self.containing_dir = os.path.dirname(self.file_path) self.encoding = encoding self.file_type = file_type self.x509 = x509 def __str__(self): data = '' if not self.x509: self.load() if self.file_type is TLSFileType.KEY: data = crypto.dump_privatekey( self.encoding, self.x509).decode("utf-8") else: data = crypto.dump_certificate( self.encoding, self.x509).decode("utf-8") return data def get_extension_value(self, ext_name): if self.is_private(): return if not self.x509: self.load() num_extensions = self.x509.get_extension_count() for i in range(num_extensions): ext = self.x509.get_extension(i) if ext: short_name = ext.get_short_name().decode('utf-8') if short_name == ext_name: return str(ext) def is_ca(self): if self.is_private(): return False if not self.x509: self.load() ext_value = self.get_extension_value('basicConstraints') if ext_value: return "CA:TRUE" in ext_value else: return False def is_private(self): """Is this a private key""" return True if self.file_type is TLSFileType.KEY else False def load(self): """Load from a file and return an x509 object""" private = self.is_private() with open_tls_file(self.file_path, 'r', private=private) as fh: if private: self.x509 = crypto.load_privatekey(self.encoding, fh.read()) else: self.x509 = crypto.load_certificate(self.encoding, fh.read()) return self.x509 def save(self, x509): """Persist this x509 object to disk""" self.x509 = x509 with open_tls_file(self.file_path, 'w', private=self.is_private()) as fh: fh.write(str(self)) class TLSFileBundle(): """Maintains information that is shared by a set of TLSFiles""" def __init__(self, common_name, files=None, x509s=None, serial=0, is_ca=False, parent_ca='', signees=None): self.record = {} self.record['serial'] = serial self.record['is_ca'] = is_ca self.record['parent_ca'] = parent_ca self.record['signees'] = signees for t in TLSFileType: setattr(self, t.value, None) files = files or {} x509s = x509s or {} self._setup_tls_files(files) self.save_x509s(x509s) def _setup_tls_files(self, files): """Initiates TLSFIle objects with the paths given to this bundle""" for file_type in TLSFileType: if file_type.value in files: file_path = files[file_type.value] setattr(self, file_type.value, TLSFile(file_path, file_type=file_type)) def save_x509s(self, x509s): """Saves the x509 objects to the paths known by this bundle""" for file_type in TLSFileType: if file_type.value in x509s: x509 = x509s[file_type.value] if file_type is not TLSFileType.CA: # persist this key or cert to disk tlsfile = getattr(self, file_type.value) if tlsfile: tlsfile.save(x509) def load_all(self): """Utility to load bring all files into memory""" for t in TLSFileType: self[t.value].load() return self def is_ca(self): """Is this bundle for a CA certificate""" return self.record['is_ca'] def to_record(self): """Create a CertStore record from this TLSFileBundle""" tf_list = [getattr(self, k, None) for k in [_.value for _ in TLSFileType]] # If a cert isn't defined in this bundle, remove it tf_list = filter(lambda x: x, tf_list) files = {tf.file_type.value: tf.file_path for tf in tf_list} self.record['files'] = files return self.record def from_record(self, record): """Build a bundle from a CertStore record""" self.record = record self._setup_tls_files(self.record['files']) return self class CertStore(): """Maintains records of certificates created by Certipy Minimally, each record keyed by common name needs: - file - path - type - serial number - parent CA - signees Common names, for the sake of simplicity, are assumed to be unique. If a pair of certs need to be valid for the same IP/DNS address (ex: localhost), that information can be specified in the Subject Alternative Name field. """ def __init__(self, containing_dir='out', store_file='certipy.json', remove_existing=False): self.store = {} self.containing_dir = containing_dir self.store_file_path = os.path.join(containing_dir, store_file) try: if remove_existing: shutil.rmtree(containing_dir) os.stat(containing_dir) self.load() except FileNotFoundError: os.makedirs(containing_dir, mode=0o755, exist_ok=True) finally: os.chmod(containing_dir, mode=0o755) def save(self): """Write the store dict to a file specified by store_file_path""" with open(self.store_file_path, 'w') as fh: fh.write(json.dumps(self.store, indent=4)) def load(self): """Read the store dict from file""" with open(self.store_file_path, 'r') as fh: self.store = json.loads(fh.read()) def get_record(self, common_name): """Return the record associated with this common name In most cases, all that's really needed to use an existing cert are the file paths to the files that make up that cert. This method returns just that and doesn't bother loading the associated files. """ try: record = self.store[common_name] return record except KeyError as e: raise CertNotFoundError( "Unable to find record of {name}" .format(name=common_name), errors=e) def get_files(self, common_name): """Return a bundle of TLS files associated with a common name""" record = self.get_record(common_name) return TLSFileBundle(common_name).from_record(record) def add_record(self, common_name, serial=0, parent_ca='', signees=None, files=None, record=None, is_ca=False, overwrite=False): """Manually create a record of certs Generally, Certipy can be left to manage certificate locations and storage, but it is occasionally useful to keep track of a set of certs that were created externally (for example, let's encrypt) """ if not overwrite: try: self.get_record(common_name) raise CertExistsError( "Certificate {name} already exists!" " Set overwrite=True to force add." .format(name=common_name)) except CertNotFoundError: pass record = record or { 'serial': serial, 'is_ca': is_ca, 'parent_ca': parent_ca, 'signees': signees, 'files': files, } self.store[common_name] = record self.save() def add_files(self, common_name, x509s, files=None, parent_ca='', is_ca=False, signees=None, serial=0, overwrite=False): """Add a set files comprising a certificate to Certipy Used with all the defaults, Certipy will manage creation of file paths to be used to store these files to disk and automatically calls save on all TLSFiles that it creates (and where it makes sense to). """ if common_name in self.store and not overwrite: raise CertExistsError( "Certificate {name} already exists!" " Set overwrite=True to force add." .format(name=common_name)) elif common_name in self.store and overwrite: record = self.get_record(common_name) serial = int(record['serial']) record['serial'] = serial + 1 TLSFileBundle(common_name).from_record(record).save_x509s(x509s) else: file_base_tmpl = "{prefix}/{cn}/{cn}" file_base = file_base_tmpl.format( prefix=self.containing_dir, cn=common_name ) try: ca_record = self.get_record(parent_ca) ca_file = ca_record['files']['cert'] except CertNotFoundError: ca_file = '' files = files or { 'key': file_base + '.key', 'cert': file_base + '.crt', 'ca': ca_file, } bundle = TLSFileBundle( common_name, files=files, x509s=x509s, is_ca=is_ca, serial=serial, parent_ca=parent_ca, signees=signees) self.store[common_name] = bundle.to_record() self.save() def add_sign_link(self, ca_name, signee_name): """Adds to the CA signees and a parent ref to the signee""" ca_record = self.get_record(ca_name) signee_record = self.get_record(signee_name) signees = ca_record['signees'] or {} signees = Counter(signees) if signee_name not in signees: signees[signee_name] = 1 ca_record['signees'] = signees signee_record['parent_ca'] = ca_name self.save() def remove_sign_link(self, ca_name, signee_name): """Removes signee_name to the signee list for ca_name""" ca_record = self.get_record(ca_name) signee_record = self.get_record(signee_name) signees = ca_record['signees'] or {} signees = Counter(signees) if signee_name in signees: signees[signee_name] = 0 ca_record['signees'] = signees signee_record['parent_ca'] = '' self.save() def update_record(self, common_name, **fields): """Update fields in an existing record""" record = self.get_record(common_name) if fields is not None: for field, value in fields: record[field] = value self.save() return record def remove_record(self, common_name): """Delete the record associated with this common name""" bundle = self.get_files(common_name) num_signees = len(Counter(bundle.record['signees'])) if bundle.is_ca() and num_signees > 0: raise CertificateAuthorityInUseError( "Authority {name} has signed {x} certificates" .format(name=common_name, x=num_signees) ) try: ca_name = bundle.record['parent_ca'] ca_record = self.get_record(ca_name) self.remove_sign_link(ca_name, common_name) except CertNotFoundError: pass record_copy = dict(self.store[common_name]) del self.store[common_name] self.save() return record_copy def remove_files(self, common_name, delete_dir=False): """Delete files and record associated with this common name""" record = self.remove_record(common_name) if delete_dir: delete_dirs = [] if 'files' in record: key_containing_dir = os.path.dirname(record['files']['key']) delete_dirs.append(key_containing_dir) cert_containing_dir = os.path.dirname(record['files']['cert']) if key_containing_dir != cert_containing_dir: delete_dirs.append(cert_containing_dir) for d in delete_dirs: shutil.rmtree(d) return record class Certipy(): def __init__(self, store_dir='out', store_file='certipy.json', remove_existing=False): self.store = CertStore(containing_dir=store_dir, store_file=store_file, remove_existing=remove_existing) def create_key_pair(self, cert_type, bits): """ Create a public/private key pair. Arguments: type - Key type, must be one of TYPE_RSA and TYPE_DSA bits - Number of bits to use in the key Returns: The public/private key pair in a PKey object """ pkey = crypto.PKey() pkey.generate_key(cert_type, bits) return pkey def create_request(self, pkey, digest="sha256", **name): """ Create a certificate request. Arguments: pkey - The key to associate with the request digest - Digestion method to use for signing, default is sha256 exts - X509 extensions see: https://www.openssl.org/docs/manmaster/man5/ x509v3_config.html#STANDARD-EXTENSIONS Dict in format: key -> (val, critical) **name - The name of the subject of the request, possible arguments are: C - Country name ST - State or province name L - Locality name O - Organization name OU - Organizational unit name CN - Common name emailAddress - E-mail address Returns: The certificate request in an X509Req object """ req = crypto.X509Req() subj = req.get_subject() if name is not None: for key, value in name.items(): setattr(subj, key, value) req.set_pubkey(pkey) req.sign(pkey, digest) return req def sign(self, req, issuer_cert_key, validity_period, digest="sha256", extensions=None, serial=0): """ Generate a certificate given a certificate request. Arguments: req - Certificate request to use issuer_cert - The certificate of the issuer issuer_key - The private key of the issuer not_before - Timestamp (relative to now) when the certificate starts being valid not_after - Timestamp (relative to now) when the certificate stops being valid digest - Digest method to use for signing, default is sha256 Returns: The signed certificate in an X509 object """ issuer_cert, issuer_key = issuer_cert_key not_before, not_after = validity_period cert = crypto.X509() cert.set_serial_number(serial) cert.gmtime_adj_notBefore(not_before) cert.gmtime_adj_notAfter(not_after) cert.set_issuer(issuer_cert.get_subject()) cert.set_subject(req.get_subject()) cert.set_pubkey(req.get_pubkey()) if extensions: for ext in extensions: if callable(ext): ext = ext(cert) cert.add_extensions([ext]) cert.sign(issuer_key, digest) return cert def create_ca_bundle_for_names(self, bundle_name, names): """Create a CA bundle to trust only certs defined in names """ records = [rec for name, rec in self.store.store.items() if name in names] return self.create_bundle( bundle_name, names=[r['parent_ca'] for r in records]) def create_ca_bundle(self, bundle_name, ca_names=None): """ Create a bundle of CA public certs for trust distribution Deprecated: 0.1.2 Arguments: ca_names - The names of CAs to include in the bundle bundle_name - The name of the bundle file to output Returns: Path to the bundle file """ return self.create_bundle(bundle_name, names=ca_names) def create_bundle(self, bundle_name, names=None, ca_only=True): """Create a bundle of public certs for trust distribution This will create a bundle of both CAs and/or regular certificates. Arguments: names - The names of certs to include in the bundle bundle_name - The name of the bundle file to output Returns: Path to the bundle file """ if not names: if ca_only: names = [] for name, record in self.store.store.items(): if record['is_ca']: names.append(name) else: names = self.store.store.keys() out_file_path = os.path.join(self.store.containing_dir, bundle_name) with open(out_file_path, 'w') as fh: for name in names: bundle = self.store.get_files(name) bundle.cert.load() fh.write(str(bundle.cert)) return out_file_path def trust_from_graph(self, graph): """Create a set of trust bundles from a relationship graph. Components in this sense are defined by unique CAs. This method assists in setting up complicated trust between various components that need to do TLS auth. Arguments: graph - dict component:list(components) Returns: dict component:trust bundle file path """ # Ensure there are CAs backing all graph components def distinct_components(graph): """Return a set of components from the provided graph.""" components = set(graph.keys()) for trusts in graph.values(): components |= set(trusts) return components # Default to creating a CA (incapable of signing intermediaries) to # identify a component not known to Certipy for component in distinct_components(graph): try: self.store.get_record(component) except CertNotFoundError: self.create_ca(component) # Build bundles from the graph trust_files = {} for component, trusts in graph.items(): file_name = component + '_trust.crt' trust_files[component] = self.create_bundle( file_name, names=trusts, ca_only=False) return trust_files def create_ca(self, name, ca_name='', cert_type=crypto.TYPE_RSA, bits=2048, alt_names=None, years=5, serial=0, pathlen=0, overwrite=False): """ Create a certificate authority Arguments: name - The name of the CA cert_type - The type of the cert. TYPE_RSA or TYPE_DSA bits - The number of bits to use alt_names - An array of alternative names in the format: IP:address, DNS:address Returns: KeyCertPair for the new CA """ cakey = self.create_key_pair(cert_type, bits) req = self.create_request(cakey, CN=name) signing_key = cakey signing_cert = req parent_ca = '' if ca_name: ca_bundle = self.store.get_files(ca_name) signing_key = ca_bundle.key.load() signing_cert = ca_bundle.cert.load() parent_ca = ca_bundle.cert.file_path basicConstraints = "CA:true" # If pathlen is exactly 0, this CA cannot sign intermediaries. # A negative value leaves this out entirely and allows arbitrary # numbers of intermediates. if pathlen >=0: basicConstraints += ', pathlen:' + str(pathlen) extensions = [ crypto.X509Extension( b"basicConstraints", True, basicConstraints.encode()), crypto.X509Extension( b"keyUsage", True, b"keyCertSign, cRLSign"), crypto.X509Extension( b"extendedKeyUsage", True, b"serverAuth, clientAuth"), lambda cert: crypto.X509Extension( b"subjectKeyIdentifier", False, b"hash", subject=cert), lambda cert: crypto.X509Extension( b"authorityKeyIdentifier", False, b"keyid:always", issuer=cert), ] if alt_names: extensions.append( crypto.X509Extension(b"subjectAltName", False, ",".join(alt_names).encode()) ) # TODO: start time before today for clock skew? cacert = self.sign( req, (signing_cert, signing_key), (0, 60*60*24*365*years), extensions=extensions) x509s = {'key': cakey, 'cert': cacert, 'ca': cacert} self.store.add_files(name, x509s, overwrite=overwrite, parent_ca=parent_ca, is_ca=True) if ca_name: self.store.add_sign_link(ca_name, name) return self.store.get_record(name) def create_signed_pair(self, name, ca_name, cert_type=crypto.TYPE_RSA, bits=2048, years=5, alt_names=None, serial=0, overwrite=False): """ Create a key-cert pair Arguments: name - The name of the key-cert pair ca_name - The name of the CA to sign this cert cert_type - The type of the cert. TYPE_RSA or TYPE_DSA bits - The number of bits to use alt_names - An array of alternative names in the format: IP:address, DNS:address Returns: KeyCertPair for the new signed pair """ key = self.create_key_pair(cert_type, bits) req = self.create_request(key, CN=name) extensions = [ crypto.X509Extension( b"extendedKeyUsage", True, b"serverAuth, clientAuth"), ] if alt_names: extensions.append( crypto.X509Extension(b"subjectAltName", False, ",".join(alt_names).encode()) ) ca_bundle = self.store.get_files(ca_name) cacert = ca_bundle.cert.load() cakey = ca_bundle.key.load() cert = self.sign(req, (cacert, cakey), (0, 60*60*24*365*years), extensions=extensions) x509s = {'key': key, 'cert': cert, 'ca': None} self.store.add_files(name, x509s, parent_ca=ca_name, overwrite=overwrite) # Relate these certs as being parent and child self.store.add_sign_link(ca_name, name) return self.store.get_record(name) certipy-0.1.3/certipy/command_line.py000066400000000000000000000143261404031340300176460ustar00rootroot00000000000000############################################################################### # Copyright (c) 2018, Lawrence Livermore National Security, LLC # Produced at the Lawrence Livermore National Laboratory # Written by Thomas Mendoza mendoza33@llnl.gov # LLNL-CODE-754897 # All rights reserved # # This file is part of Certipy. For details, see # https://github.com/LLNL/certipy. Please also read this link - Additional # BSD Notice. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the disclaimer below. # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the disclaimer (as noted below) in # the documentation and/or other materials provided with the distribution. # * Neither the name of the LLNS/LLNL nor the names of its contributors may # be used to endorse or promote products derived from this software without # specific prior written permission. # # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL LAWRENCE LIVERMORE NATIONAL SECURITY, LLC, # THE U.S. DEPARTMENT OF ENERGY OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # # Additional BSD Notice # # 1. This notice is required to be provided under our contract with the U.S. # Department of Energy (DOE). This work was produced at Lawrence Livermore # National Laboratory under Contract No. DE-AC52-07NA27344 with the DOE. # # 2. Neither the United States Government nor Lawrence Livermore National # Security, LLC nor any of their employees, makes any warranty, express or # implied, or assumes any liability or responsibility for the accuracy, # completeness, or usefulness of any information, apparatus, product, # or process disclosed, or represents that its use would not infringe # privately-owned rights. # # 3. Also, reference herein to any specific commercial products, process, or # services by trade name, trademark, manufacturer or otherwise does not # necessarily constitute or imply its endorsement, recommendation, or favoring # by the United States Government or Lawrence Livermore National Security, LLC. # The views and opinions of authors expressed herein do not necessarily state # or reflect those of the United States Government or Lawrence Livermore # National Security, LLC, and shall not be used for advertising or product # endorsement purposes. ############################################################################### import argparse import shutil import sys from OpenSSL import crypto from certipy import ( Certipy, CertExistsError, CertNotFoundError, CertificateAuthorityInUseError ) def main(): describe_certipy = """ Certipy: Create simple, self-signed certificate authorities and certs. """ parser = argparse.ArgumentParser(description=describe_certipy) parser.add_argument( 'name', help="""Name of the cert to create, defaults to creating a CA cert. If no signing --ca-name specified.""") parser.add_argument( '--ca-name', help="The name of the CA to sign this cert." , default="") parser.add_argument( '--overwrite', action="store_true", help="If the cert already exists, bump the serial and overwrite it.") parser.add_argument( '--rm', action="store_true", help="Remove the cert specified by name.") parser.add_argument( '--cert-type', default="rsa", choices=['rsa', 'dsa'], help="The type of cert to create.") parser.add_argument( '--bits', type=int, default=2048, help="The number of bits to use.") parser.add_argument( '--valid', type=int, default=5, help="Years the cert is valid for.") parser.add_argument( '--alt-names', default="", help="Alt names for the certificate (comma delimited).") parser.add_argument( '--store-dir', default="out", help="The location for the store and certs.") args = parser.parse_args() certipy = Certipy(store_dir=args.store_dir) cert_type = crypto.TYPE_RSA if args.cert_type is "rsa" else crypto.TYPE_DSA record = None if args.rm: try: record = certipy.store.remove_files(args.name, delete_dir=True) print("Deleted:") for key, val in record.items(): print(key.upper(), val) except CertificateAuthorityInUseError as e: print("Unable to delete.", e) sys.exit(0) alt_names = None if args.alt_names: alt_names = [_.strip() for _ in args.alt_names.split(',')] if args.ca_name: ca_record = certipy.store.get_record(args.ca_name) if ca_record: try: record = certipy.create_signed_pair( args.name, args.ca_name, cert_type=cert_type, bits=args.bits, years=args.valid, alt_names=alt_names, overwrite=args.overwrite) except CertExistsError as e: print(e) else: print( "CA {} not found. Must specify an exisiting authority to" " sign this cert.".format(args.ca_name)) else: try: record = certipy.create_ca( args.name, cert_type=cert_type, bits=args.bits, years=args.valid, alt_names=alt_names, overwrite=args.overwrite) except CertExistsError as e: print(e) if record: for key, val in record.items(): print(key.upper(), val) certipy-0.1.3/certipy/test/000077500000000000000000000000001404031340300156205ustar00rootroot00000000000000certipy-0.1.3/certipy/test/__init__.py000066400000000000000000000063311404031340300177340ustar00rootroot00000000000000############################################################################### # Copyright (c) 2018, Lawrence Livermore National Security, LLC # Produced at the Lawrence Livermore National Laboratory # Written by Thomas Mendoza mendoza33@llnl.gov # LLNL-CODE-754897 # All rights reserved # # This file is part of Certipy. For details, see https://github.com/LLNL/certipy. # Please also read this link - Additional BSD Notice. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the disclaimer below. # * Redistributions in binary form must reproduce the above copyright notice, # this list of conditions and the disclaimer (as noted below) in the # documentation and/or other materials provided with the distribution. # * Neither the name of the LLNS/LLNL nor the names of its contributors may # be used to endorse or promote products derived from this software without # specific prior written permission. # # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL LAWRENCE LIVERMORE NATIONAL SECURITY, LLC, # THE U.S. DEPARTMENT OF ENERGY OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # # Additional BSD Notice # # 1. This notice is required to be provided under our contract with the U.S. # Department of Energy (DOE). This work was produced at Lawrence Livermore # National Laboratory under Contract No. DE-AC52-07NA27344 with the DOE. # # 2. Neither the United States Government nor Lawrence Livermore National # Security, LLC nor any of their employees, makes any warranty, express or # implied, or assumes any liability or responsibility for the accuracy, # completeness, or usefulness of any information, apparatus, product, # or process disclosed, or represents that its use would not infringe # privately-owned rights. # # 3. Also, reference herein to any specific commercial products, process, or # services by trade name, trademark, manufacturer or otherwise does not # necessarily constitute or imply its endorsement, recommendation, or favoring # by the United States Government or Lawrence Livermore National Security, LLC. # The views and opinions of authors expressed herein do not necessarily state or # reflect those of the United States Government or Lawrence Livermore National # Security, LLC, and shall not be used for advertising or product endorsement # purposes. ############################################################################### certipy-0.1.3/certipy/test/test_certipy.py000066400000000000000000000313131404031340300207110ustar00rootroot00000000000000############################################################################### # Copyright (c) 2018, Lawrence Livermore National Security, LLC # Produced at the Lawrence Livermore National Laboratory # Written by Thomas Mendoza mendoza33@llnl.gov # LLNL-CODE-754897 # All rights reserved # # This file is part of Certipy. For details, see # https://github.com/LLNL/certipy. Please also read this link - Additional # BSD Notice. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the disclaimer below. # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the disclaimer (as noted below) in # the documentation and/or other materials provided with the distribution. # * Neither the name of the LLNS/LLNL nor the names of its contributors may # be used to endorse or promote products derived from this software without # specific prior written permission. # # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL LAWRENCE LIVERMORE NATIONAL SECURITY, LLC, # THE U.S. DEPARTMENT OF ENERGY OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # # Additional BSD Notice # # 1. This notice is required to be provided under our contract with the U.S. # Department of Energy (DOE). This work was produced at Lawrence Livermore # National Laboratory under Contract No. DE-AC52-07NA27344 with the DOE. # # 2. Neither the United States Government nor Lawrence Livermore National # Security, LLC nor any of their employees, makes any warranty, express or # implied, or assumes any liability or responsibility for the accuracy, # completeness, or usefulness of any information, apparatus, product, # or process disclosed, or represents that its use would not infringe # privately-owned rights. # # 3. Also, reference herein to any specific commercial products, process, or # services by trade name, trademark, manufacturer or otherwise does not # necessarily constitute or imply its endorsement, recommendation, or favoring # by the United States Government or Lawrence Livermore National Security, LLC. # The views and opinions of authors expressed herein do not necessarily state # or reflect those of the United States Government or Lawrence Livermore # National Security, LLC, and shall not be used for advertising or product # endorsement purposes. ############################################################################### import os import pytest import shutil from pytest import fixture from OpenSSL import crypto from pathlib import Path from tempfile import NamedTemporaryFile, TemporaryDirectory from ..certipy import ( TLSFileType, TLSFile, TLSFileBundle, CertStore, open_tls_file, CertExistsError, Certipy ) @fixture(scope='module') def signed_key_pair(): pkey = crypto.PKey() pkey.generate_key(crypto.TYPE_RSA, 2048) req = crypto.X509Req() subj = req.get_subject() setattr(subj, 'CN', 'test') req.set_pubkey(pkey) req.sign(pkey, 'sha256') issuer_cert, issuer_key = (req, pkey) not_before, not_after = (0, 60*60*24*365*2) cert = crypto.X509() cert.set_serial_number(0) cert.gmtime_adj_notBefore(not_before) cert.gmtime_adj_notAfter(not_after) cert.set_issuer(issuer_cert.get_subject()) cert.set_subject(req.get_subject()) cert.set_pubkey(req.get_pubkey()) cert.sign(issuer_key, 'sha256') return (pkey, cert) @fixture(scope='module') def record(): return { 'serial': 0, 'parent_ca': '', 'signees': None, 'files': { 'key': 'out/foo.key', 'cert': 'out/foo.crt', 'ca': 'out/ca.crt', }, } def test_tls_context_manager(): def simple_perms(f): return oct(os.stat(f).st_mode & 0o777) # read with pytest.raises(OSError) as e: with open_tls_file('foo.test', 'r') as tlsfh: pass with NamedTemporaryFile('w') as fh: with open_tls_file(fh.name, 'r') as tlsfh: pass # write with NamedTemporaryFile('w') as fh: containing_dir = os.path.dirname(fh.name) # public certificate with open_tls_file(fh.name, 'w', private=False) as tlsfh: assert simple_perms(containing_dir) == '0o755' assert simple_perms(fh.name) == '0o644' # private certificate with open_tls_file(fh.name, 'w') as tlsfh: assert simple_perms(containing_dir) == '0o755' assert simple_perms(fh.name) == '0o600' def test_tls_file(signed_key_pair): key, cert = signed_key_pair def read_write_key(file_type): with NamedTemporaryFile('w') as fh: tlsfile = TLSFile(fh.name, file_type=file_type) # test persist to disk x509 = cert if file_type is TLSFileType.CERT else key tlsfile.save(x509) with open(fh.name, 'r') as f: assert f.read() is not None # test load from disk loaded_tlsfile = TLSFile(fh.name, file_type=file_type) loaded_tlsfile.x509 = tlsfile.load() assert str(loaded_tlsfile) == str(tlsfile) # public key read_write_key(TLSFileType.CERT) # private key read_write_key(TLSFileType.KEY) def test_tls_file_bundle(signed_key_pair, record): key, cert = signed_key_pair # from record bundle = TLSFileBundle('foo').from_record(record) assert bundle.key and bundle.cert and bundle.ca # to record exported_record = bundle.to_record() f_types = {key for key in exported_record['files'].keys()} assert len(f_types) == 3 assert f_types == {'key', 'cert', 'ca'} def test_certipy_store(signed_key_pair, record): key, cert = signed_key_pair key_str = crypto.dump_privatekey(crypto.FILETYPE_PEM, key)\ .decode('utf-8') cert_str = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)\ .decode('utf-8') with TemporaryDirectory() as td: common_name = 'foo' store = CertStore(containing_dir=td) # add files x509s = { 'key': key, 'cert': cert, 'ca': None, } store.add_files(common_name, x509s) # check the TLSFiles bundle = store.get_files(common_name) bundle.key.load() bundle.cert.load() assert key_str == str(bundle.key) assert cert_str == str(bundle.cert) # save the store records to a file store.save() # read the records back in store.load() # check the record for those files main_record = store.get_record(common_name) non_empty_paths = [f for f in main_record['files'].values() if f] assert len(non_empty_paths) == 2 # add another record with no physical files signee_common_name = 'bar' store.add_record(signee_common_name, record=record) # 'sign' cert store.add_sign_link(common_name, signee_common_name) signee_record = store.get_record(signee_common_name) assert len(main_record['signees']) == 1 assert signee_record['parent_ca'] == common_name def test_certipy(): # FIXME: unfortunately similar names...either separate tests or rename with TemporaryDirectory() as td: # create a CA ca_name = 'foo' certipy = Certipy(store_dir=td) ca_record = certipy.create_ca(ca_name, pathlen=-1) non_empty_paths = [f for f in ca_record['files'].values() if f] assert len(non_empty_paths) == 2 # check that the paths are backed by actual files ca_bundle = certipy.store.get_files(ca_name) assert ca_bundle.key.load() is not None assert ca_bundle.cert.load() is not None assert 'PRIVATE' in str(ca_bundle.key) assert 'CERTIFICATE' in str(ca_bundle.cert) # create a cert and sign it with that CA cert_name = 'bar' alt_names = ['DNS:bar.example.com', 'IP:10.10.10.10'] cert_record = certipy.create_signed_pair( cert_name, ca_name, alt_names=alt_names) non_empty_paths = [f for f in cert_record['files'].values() if f] assert len(non_empty_paths) == 3 assert cert_record['files']['ca'] == ca_record['files']['cert'] cert_bundle = certipy.store.get_files(cert_name) stored_alt_names = cert_bundle.cert.get_extension_value( 'subjectAltName') assert alt_names[0] in stored_alt_names # For some reason, the string representation changes IP: to # IP Address:... the important part is that the actual IP is in the # extension. assert alt_names[1][3:] in stored_alt_names # add a second CA ca_name1 = 'baz' certipy.create_ca(ca_name1) # create a bundle from all known certs bundle_file_name = 'bundle.crt' bundle_file = certipy.create_ca_bundle(bundle_file_name) ca_bundle1 = certipy.store.get_files(ca_name1) ca_bundle1.cert.load() assert bundle_file is not None with open(bundle_file, 'r') as fh: all_certs = fh.read() # should contain both CA certs assert str(ca_bundle.cert) in all_certs assert str(ca_bundle1.cert) in all_certs # bundle of CA certs for only a single name this time bundle_file = certipy.create_ca_bundle_for_names(bundle_file_name, ['bar']) assert bundle_file is not None with open(bundle_file, 'r') as fh: all_certs = fh.read() assert str(ca_bundle.cert) in all_certs assert str(ca_bundle1.cert) not in all_certs # delete certs deleted_record = certipy.store.remove_files('bar', delete_dir=True) assert not os.path.exists(deleted_record['files']['cert']) assert not os.path.exists(deleted_record['files']['key']) # the CA cert should still be around, we have to delete that explicitly assert os.path.exists(deleted_record['files']['ca']) # create an intermediate CA begin_ca_signee_num = len(ca_record['signees'] or {}) intermediate_ca = 'bat' intermediate_ca_record = certipy.create_ca( intermediate_ca, ca_name=ca_name, pathlen=1) end_ca_signee_num = len(ca_record['signees']) intermediate_ca_bundle = certipy.store.get_files(intermediate_ca) basic_constraints = intermediate_ca_bundle.cert.get_extension_value( 'basicConstraints') assert end_ca_signee_num > begin_ca_signee_num assert intermediate_ca_bundle.record['parent_ca'] == ca_name assert intermediate_ca_bundle.is_ca() assert 'pathlen:1' in basic_constraints def test_certipy_trust_graph(): trust_graph = { 'foo': ['foo', 'bar'], 'bar': ['foo'], 'baz': ['bar'], } def distinct_components(graph): """Return a set of components from the provided graph.""" components = set(graph.keys()) for trusts in graph.values(): components |= set(trusts) return components with TemporaryDirectory() as td: certipy = Certipy(store_dir=td) # after this, all components in the graph should exist in certipy trust_files = certipy.trust_from_graph(trust_graph) bundles = {} all_components = distinct_components(trust_graph) for component in all_components: bundles[component] = certipy.store.get_files(component) # components should only trust others listed explicitly in the graph for component, trusts in trust_graph.items(): trust_file = trust_files[component] not_trusts = all_components - set(trusts) with open(trust_file) as fh: trust_bundle = fh.read() for trusted_comp in trusts: bundle = bundles[trusted_comp] assert str(bundle.cert) in trust_bundle for untrusted_comp in not_trusts: bundle = bundles[untrusted_comp] assert str(bundle.cert) not in trust_bundle certipy-0.1.3/setup.cfg000066400000000000000000000000461404031340300150030ustar00rootroot00000000000000[egg_info] tag_build = tag_date = 0 certipy-0.1.3/setup.py000066400000000000000000000117011404031340300146740ustar00rootroot00000000000000############################################################################### # Copyright (c) 2018, Lawrence Livermore National Security, LLC # Produced at the Lawrence Livermore National Laboratory # Written by Thomas Mendoza mendoza33@llnl.gov # LLNL-CODE-754897 # All rights reserved # # This file is part of Certipy. For details, see # https://github.com/LLNL/certipy. Please also read this link - Additional # BSD Notice. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright notice, # this list of conditions and the disclaimer below. # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the disclaimer (as noted below) in # the documentation and/or other materials provided with the distribution. # * Neither the name of the LLNS/LLNL nor the names of its contributors may # be used to endorse or promote products derived from this software without # specific prior written permission. # # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL LAWRENCE LIVERMORE NATIONAL SECURITY, LLC, # THE U.S. DEPARTMENT OF ENERGY OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # # Additional BSD Notice # # 1. This notice is required to be provided under our contract with the U.S. # Department of Energy (DOE). This work was produced at Lawrence Livermore # National Laboratory under Contract No. DE-AC52-07NA27344 with the DOE. # # 2. Neither the United States Government nor Lawrence Livermore National # Security, LLC nor any of their employees, makes any warranty, express or # implied, or assumes any liability or responsibility for the accuracy, # completeness, or usefulness of any information, apparatus, product, # or process disclosed, or represents that its use would not infringe # privately-owned rights. # # 3. Also, reference herein to any specific commercial products, process, or # services by trade name, trademark, manufacturer or otherwise does not # necessarily constitute or imply its endorsement, recommendation, or favoring # by the United States Government or Lawrence Livermore National Security, LLC. # The views and opinions of authors expressed herein do not necessarily state # or reflect those of the United States Government or Lawrence Livermore # National Security, LLC, and shall not be used for advertising or product # endorsement purposes. ############################################################################### # Always prefer setuptools over distutils from setuptools import setup, find_packages # To use a consistent encoding from codecs import open from os import path from setuptools import setup try: from pypandoc import convert read_md = lambda f: convert(f, 'rst', 'md') except ImportError: print("warning: pypandoc module not found, could not convert Markdown to RST") read_md = lambda f: f here = path.abspath(path.dirname(__file__)) # Get the long description from the README file with open(path.join(here, 'README.md'), encoding='utf-8') as f: long_description = read_md(f.read()) setup( name='certipy', version='0.1.3', description='Utility to create and sign CAs and certificates', long_description=long_description, url='https://github.com/LLNL/certipy', author='Thomas Mendoza', author_email='mendoza33@llnl.gov', license='BSD', # See https://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers=[ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'Topic :: Utilities', 'License :: OSI Approved :: BSD License', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', ], keywords='pki ssl tls certificates', packages=find_packages(exclude=['contrib', 'docs', 'test']), install_requires=['pyopenssl'], extras_require={ 'dev': ['pytest'], 'test': ['pytest'], }, package_data={ }, data_files=[], entry_points={ 'console_scripts': [ 'certipy=certipy.command_line:main', ], }, )