"Fossies" - the Fresh Open Source Software Archive

Member "salt-3002.2/salt/modules/tls.py" (18 Nov 2020, 60049 Bytes) of package /linux/misc/salt-3002.2.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "tls.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 3002.1_vs_3002.2.

    1 r"""
    2 A salt module for SSL/TLS.  Can create a Certificate Authority (CA)
    3 or use Self-Signed certificates.
    4 
    5 :depends: PyOpenSSL Python module (0.10 or later, 0.14 or later for X509
    6     extension support)
    7 
    8 :configuration: Add the following values in /etc/salt/minion for the CA module
    9     to function properly:
   10 
   11     .. code-block:: yaml
   12 
   13         ca.cert_base_path: '/etc/pki'
   14 
   15 
   16 CLI Example #1:
   17 Creating a CA, a server request and its signed certificate:
   18 
   19 .. code-block:: bash
   20 
   21     # salt-call tls.create_ca my_little \
   22     days=5 \
   23     CN='My Little CA' \
   24     C=US \
   25     ST=Utah \
   26     L=Salt Lake City \
   27     O=Saltstack \
   28     emailAddress=pleasedontemail@example.com
   29 
   30     Created Private Key: "/etc/pki/my_little/my_little_ca_cert.key"
   31     Created CA "my_little_ca": "/etc/pki/my_little_ca/my_little_ca_cert.crt"
   32 
   33     # salt-call tls.create_csr my_little CN=www.example.com
   34     Created Private Key: "/etc/pki/my_little/certs/www.example.com.key
   35     Created CSR for "www.example.com": "/etc/pki/my_little/certs/www.example.com.csr"
   36 
   37     # salt-call tls.create_ca_signed_cert my_little CN=www.example.com
   38     Created Certificate for "www.example.com": /etc/pki/my_little/certs/www.example.com.crt"
   39 
   40 CLI Example #2:
   41 Creating a client request and its signed certificate
   42 
   43 .. code-block:: bash
   44 
   45     # salt-call tls.create_csr my_little CN=DBReplica_No.1 cert_type=client
   46     Created Private Key: "/etc/pki/my_little/certs//DBReplica_No.1.key."
   47     Created CSR for "DBReplica_No.1": "/etc/pki/my_little/certs/DBReplica_No.1.csr."
   48 
   49     # salt-call tls.create_ca_signed_cert my_little CN=DBReplica_No.1
   50     Created Certificate for "DBReplica_No.1": "/etc/pki/my_little/certs/DBReplica_No.1.crt"
   51 
   52 CLI Example #3:
   53 Creating both a server and client req + cert for the same CN
   54 
   55 .. code-block:: bash
   56 
   57     # salt-call tls.create_csr my_little CN=MasterDBReplica_No.2  \
   58         cert_type=client
   59     Created Private Key: "/etc/pki/my_little/certs/MasterDBReplica_No.2.key."
   60     Created CSR for "DBReplica_No.1": "/etc/pki/my_little/certs/MasterDBReplica_No.2.csr."
   61 
   62     # salt-call tls.create_ca_signed_cert my_little CN=MasterDBReplica_No.2
   63     Created Certificate for "DBReplica_No.1": "/etc/pki/my_little/certs/DBReplica_No.1.crt"
   64 
   65     # salt-call tls.create_csr my_little CN=MasterDBReplica_No.2 \
   66         cert_type=server
   67     Certificate "MasterDBReplica_No.2" already exists
   68 
   69     (doh!)
   70 
   71     # salt-call tls.create_csr my_little CN=MasterDBReplica_No.2 \
   72         cert_type=server type_ext=True
   73     Created Private Key: "/etc/pki/my_little/certs/DBReplica_No.1_client.key."
   74     Created CSR for "DBReplica_No.1": "/etc/pki/my_little/certs/DBReplica_No.1_client.csr."
   75 
   76     # salt-call tls.create_ca_signed_cert my_little CN=MasterDBReplica_No.2
   77     Certificate "MasterDBReplica_No.2" already exists
   78 
   79     (DOH!)
   80 
   81     # salt-call tls.create_ca_signed_cert my_little CN=MasterDBReplica_No.2 \
   82         cert_type=server type_ext=True
   83     Created Certificate for "MasterDBReplica_No.2": "/etc/pki/my_little/certs/MasterDBReplica_No.2_server.crt"
   84 
   85 
   86 CLI Example #4:
   87 Create a server req + cert with non-CN filename for the cert
   88 
   89 .. code-block:: bash
   90 
   91     # salt-call tls.create_csr my_little CN=www.anothersometh.ing \
   92         cert_type=server type_ext=True
   93     Created Private Key: "/etc/pki/my_little/certs/www.anothersometh.ing_server.key."
   94     Created CSR for "DBReplica_No.1": "/etc/pki/my_little/certs/www.anothersometh.ing_server.csr."
   95 
   96     # salt-call tls_create_ca_signed_cert my_little CN=www.anothersometh.ing \
   97         cert_type=server cert_filename="something_completely_different"
   98     Created Certificate for "www.anothersometh.ing": /etc/pki/my_little/certs/something_completely_different.crt
   99 """
  100 
  101 import binascii
  102 import calendar
  103 import logging
  104 import math
  105 import os
  106 import re
  107 import time
  108 from datetime import datetime
  109 
  110 import salt.utils.data
  111 import salt.utils.files
  112 import salt.utils.stringutils
  113 from salt.exceptions import CommandExecutionError
  114 from salt.utils.versions import LooseVersion as _LooseVersion
  115 
  116 # pylint: disable=C0103
  117 
  118 
  119 HAS_SSL = False
  120 X509_EXT_ENABLED = True
  121 try:
  122     import OpenSSL
  123 
  124     HAS_SSL = True
  125     OpenSSL_version = _LooseVersion(OpenSSL.__dict__.get("__version__", "0.0"))
  126 except ImportError:
  127     pass
  128 
  129 
  130 log = logging.getLogger(__name__)
  131 
  132 two_digit_year_fmt = "%y%m%d%H%M%SZ"
  133 four_digit_year_fmt = "%Y%m%d%H%M%SZ"
  134 
  135 
  136 def __virtual__():
  137     """
  138     Only load this module if the ca config options are set
  139     """
  140     global X509_EXT_ENABLED
  141     if HAS_SSL and OpenSSL_version >= _LooseVersion("0.10"):
  142         if OpenSSL_version < _LooseVersion("0.14"):
  143             X509_EXT_ENABLED = False
  144             log.debug(
  145                 "You should upgrade pyOpenSSL to at least 0.14.1 to "
  146                 "enable the use of X509 extensions in the tls module"
  147             )
  148         elif OpenSSL_version <= _LooseVersion("0.15"):
  149             log.debug(
  150                 "You should upgrade pyOpenSSL to at least 0.15.1 to "
  151                 "enable the full use of X509 extensions in the tls module"
  152             )
  153         # NOTE: Not having configured a cert path should not prevent this
  154         # module from loading as it provides methods to configure the path.
  155         return True
  156     else:
  157         X509_EXT_ENABLED = False
  158         return (
  159             False,
  160             "PyOpenSSL version 0.10 or later must be installed "
  161             "before this module can be used.",
  162         )
  163 
  164 
  165 def _microtime():
  166     """
  167     Return a Unix timestamp as a string of digits
  168     :return:
  169     """
  170     val1, val2 = math.modf(time.time())
  171     val2 = int(val2)
  172     return "{:f}{}".format(val1, val2)
  173 
  174 
  175 def cert_base_path(cacert_path=None):
  176     """
  177     Return the base path for certs from CLI or from options
  178 
  179     cacert_path
  180         absolute path to ca certificates root directory
  181 
  182     CLI Example:
  183 
  184     .. code-block:: bash
  185 
  186         salt '*' tls.cert_base_path
  187     """
  188     if not cacert_path:
  189         cacert_path = __context__.get(
  190             "ca.contextual_cert_base_path",
  191             __salt__["config.option"]("ca.contextual_cert_base_path"),
  192         )
  193     if not cacert_path:
  194         cacert_path = __context__.get(
  195             "ca.cert_base_path", __salt__["config.option"]("ca.cert_base_path")
  196         )
  197     return cacert_path
  198 
  199 
  200 def _cert_base_path(cacert_path=None):
  201     """
  202     Retrocompatible wrapper
  203     """
  204     return cert_base_path(cacert_path)
  205 
  206 
  207 def set_ca_path(cacert_path):
  208     """
  209     If wanted, store the aforementioned cacert_path in context
  210     to be used as the basepath for further operations
  211 
  212     CLI Example:
  213 
  214     .. code-block:: bash
  215 
  216         salt '*' tls.set_ca_path /etc/certs
  217     """
  218     if cacert_path:
  219         __context__["ca.contextual_cert_base_path"] = cacert_path
  220     return cert_base_path()
  221 
  222 
  223 def _new_serial(ca_name):
  224     """
  225     Return a serial number in hex using os.urandom() and a Unix timestamp
  226     in microseconds.
  227 
  228     ca_name
  229         name of the CA
  230     CN
  231         common name in the request
  232     """
  233     hashnum = int(
  234         binascii.hexlify(
  235             b"_".join((salt.utils.stringutils.to_bytes(_microtime()), os.urandom(5),))
  236         ),
  237         16,
  238     )
  239     log.debug("Hashnum: %s", hashnum)
  240 
  241     # record the hash somewhere
  242     cachedir = __opts__["cachedir"]
  243     log.debug("cachedir: %s", cachedir)
  244     serial_file = "{}/{}.serial".format(cachedir, ca_name)
  245     if not os.path.exists(cachedir):
  246         os.makedirs(cachedir)
  247     if not os.path.exists(serial_file):
  248         mode = "w"
  249     else:
  250         mode = "a+"
  251     with salt.utils.files.fopen(serial_file, mode) as ofile:
  252         ofile.write(str(hashnum))  # future lint: disable=blacklisted-function
  253 
  254     return hashnum
  255 
  256 
  257 def _four_digit_year_to_two_digit(datetimeObj):
  258     return datetimeObj.strftime(two_digit_year_fmt)
  259 
  260 
  261 def _get_basic_info(ca_name, cert, ca_dir=None):
  262     """
  263     Get basic info to write out to the index.txt
  264     """
  265     if ca_dir is None:
  266         ca_dir = "{}/{}".format(_cert_base_path(), ca_name)
  267 
  268     index_file = "{}/index.txt".format(ca_dir)
  269 
  270     cert = _read_cert(cert)
  271     expire_date = _four_digit_year_to_two_digit(_get_expiration_date(cert))
  272     serial_number = format(cert.get_serial_number(), "X")
  273 
  274     # gotta prepend a /
  275     subject = "/"
  276 
  277     # then we can add the rest of the subject
  278     subject += "/".join(
  279         ["{}={}".format(x, y) for x, y in cert.get_subject().get_components()]
  280     )
  281     subject += "\n"
  282 
  283     return (index_file, expire_date, serial_number, subject)
  284 
  285 
  286 def _write_cert_to_database(ca_name, cert, cacert_path=None, status="V"):
  287     """
  288     write out the index.txt database file in the appropriate directory to
  289     track certificates
  290 
  291     ca_name
  292         name of the CA
  293     cert
  294         certificate to be recorded
  295     """
  296     set_ca_path(cacert_path)
  297     ca_dir = "{}/{}".format(cert_base_path(), ca_name)
  298     index_file, expire_date, serial_number, subject = _get_basic_info(
  299         ca_name, cert, ca_dir
  300     )
  301 
  302     index_data = "{}\t{}\t\t{}\tunknown\t{}".format(
  303         status, expire_date, serial_number, subject
  304     )
  305 
  306     with salt.utils.files.fopen(index_file, "a+") as ofile:
  307         ofile.write(salt.utils.stringutils.to_str(index_data))
  308 
  309 
  310 def maybe_fix_ssl_version(ca_name, cacert_path=None, ca_filename=None):
  311     """
  312     Check that the X509 version is correct
  313     (was incorrectly set in previous salt versions).
  314     This will fix the version if needed.
  315 
  316     ca_name
  317         ca authority name
  318     cacert_path
  319         absolute path to ca certificates root directory
  320     ca_filename
  321         alternative filename for the CA
  322 
  323         .. versionadded:: 2015.5.3
  324 
  325 
  326     CLI Example:
  327 
  328     .. code-block:: bash
  329 
  330         salt '*' tls.maybe_fix_ssl_version test_ca /etc/certs
  331     """
  332     set_ca_path(cacert_path)
  333     if not ca_filename:
  334         ca_filename = "{}_ca_cert".format(ca_name)
  335     certp = "{}/{}/{}.crt".format(cert_base_path(), ca_name, ca_filename)
  336     ca_keyp = "{}/{}/{}.key".format(cert_base_path(), ca_name, ca_filename)
  337     with salt.utils.files.fopen(certp) as fic:
  338         cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, fic.read())
  339         if cert.get_version() == 3:
  340             log.info("Regenerating wrong x509 version " "for certificate %s", certp)
  341             with salt.utils.files.fopen(ca_keyp) as fic2:
  342                 try:
  343                     # try to determine the key bits
  344                     key = OpenSSL.crypto.load_privatekey(
  345                         OpenSSL.crypto.FILETYPE_PEM, fic2.read()
  346                     )
  347                     bits = key.bits()
  348                 except Exception:  # pylint: disable=broad-except
  349                     bits = 2048
  350                 try:
  351                     days = (
  352                         datetime.strptime(cert.get_notAfter(), "%Y%m%d%H%M%SZ")
  353                         - datetime.utcnow()
  354                     ).days
  355                 except (ValueError, TypeError):
  356                     days = 365
  357                 subj = cert.get_subject()
  358                 create_ca(
  359                     ca_name,
  360                     bits=bits,
  361                     days=days,
  362                     CN=subj.CN,
  363                     C=subj.C,
  364                     ST=subj.ST,
  365                     L=subj.L,
  366                     O=subj.O,
  367                     OU=subj.OU,
  368                     emailAddress=subj.emailAddress,
  369                     fixmode=True,
  370                 )
  371 
  372 
  373 def ca_exists(ca_name, cacert_path=None, ca_filename=None):
  374     """
  375     Verify whether a Certificate Authority (CA) already exists
  376 
  377     ca_name
  378         name of the CA
  379     cacert_path
  380         absolute path to ca certificates root directory
  381     ca_filename
  382         alternative filename for the CA
  383 
  384         .. versionadded:: 2015.5.3
  385 
  386 
  387     CLI Example:
  388 
  389     .. code-block:: bash
  390 
  391         salt '*' tls.ca_exists test_ca /etc/certs
  392     """
  393     set_ca_path(cacert_path)
  394     if not ca_filename:
  395         ca_filename = "{}_ca_cert".format(ca_name)
  396     certp = "{}/{}/{}.crt".format(cert_base_path(), ca_name, ca_filename)
  397     if os.path.exists(certp):
  398         maybe_fix_ssl_version(ca_name, cacert_path=cacert_path, ca_filename=ca_filename)
  399         return True
  400     return False
  401 
  402 
  403 def _ca_exists(ca_name, cacert_path=None):
  404     """Retrocompatible wrapper"""
  405     return ca_exists(ca_name, cacert_path)
  406 
  407 
  408 def get_ca(ca_name, as_text=False, cacert_path=None):
  409     """
  410     Get the certificate path or content
  411 
  412     ca_name
  413         name of the CA
  414     as_text
  415         if true, return the certificate content instead of the path
  416     cacert_path
  417         absolute path to ca certificates root directory
  418 
  419     CLI Example:
  420 
  421     .. code-block:: bash
  422 
  423         salt '*' tls.get_ca test_ca as_text=False cacert_path=/etc/certs
  424     """
  425     set_ca_path(cacert_path)
  426     certp = "{0}/{1}/{1}_ca_cert.crt".format(cert_base_path(), ca_name)
  427     if not os.path.exists(certp):
  428         raise ValueError("Certificate does not exist for {}".format(ca_name))
  429     else:
  430         if as_text:
  431             with salt.utils.files.fopen(certp) as fic:
  432                 certp = salt.utils.stringutils.to_unicode(fic.read())
  433     return certp
  434 
  435 
  436 def get_ca_signed_cert(
  437     ca_name, CN="localhost", as_text=False, cacert_path=None, cert_filename=None
  438 ):
  439     """
  440     Get the certificate path or content
  441 
  442     ca_name
  443         name of the CA
  444     CN
  445         common name of the certificate
  446     as_text
  447         if true, return the certificate content instead of the path
  448     cacert_path
  449         absolute path to certificates root directory
  450     cert_filename
  451         alternative filename for the certificate, useful when using special characters in the CN
  452 
  453         .. versionadded:: 2015.5.3
  454 
  455 
  456     CLI Example:
  457 
  458     .. code-block:: bash
  459 
  460         salt '*' tls.get_ca_signed_cert test_ca CN=localhost as_text=False cacert_path=/etc/certs
  461     """
  462     set_ca_path(cacert_path)
  463     if not cert_filename:
  464         cert_filename = CN
  465 
  466     certp = "{}/{}/certs/{}.crt".format(cert_base_path(), ca_name, cert_filename)
  467     if not os.path.exists(certp):
  468         raise ValueError("Certificate does not exists for {}".format(CN))
  469     else:
  470         if as_text:
  471             with salt.utils.files.fopen(certp) as fic:
  472                 certp = salt.utils.stringutils.to_unicode(fic.read())
  473     return certp
  474 
  475 
  476 def get_ca_signed_key(
  477     ca_name, CN="localhost", as_text=False, cacert_path=None, key_filename=None
  478 ):
  479     """
  480     Get the certificate path or content
  481 
  482     ca_name
  483         name of the CA
  484     CN
  485         common name of the certificate
  486     as_text
  487         if true, return the certificate content instead of the path
  488     cacert_path
  489         absolute path to certificates root directory
  490     key_filename
  491         alternative filename for the key, useful when using special characters
  492 
  493         .. versionadded:: 2015.5.3
  494 
  495         in the CN
  496 
  497     CLI Example:
  498 
  499     .. code-block:: bash
  500 
  501         salt '*' tls.get_ca_signed_key \
  502                 test_ca CN=localhost \
  503                 as_text=False \
  504                 cacert_path=/etc/certs
  505     """
  506     set_ca_path(cacert_path)
  507     if not key_filename:
  508         key_filename = CN
  509 
  510     keyp = "{}/{}/certs/{}.key".format(cert_base_path(), ca_name, key_filename)
  511     if not os.path.exists(keyp):
  512         raise ValueError("Certificate does not exists for {}".format(CN))
  513     else:
  514         if as_text:
  515             with salt.utils.files.fopen(keyp) as fic:
  516                 keyp = salt.utils.stringutils.to_unicode(fic.read())
  517     return keyp
  518 
  519 
  520 def _read_cert(cert):
  521     if isinstance(cert, str):
  522         try:
  523             with salt.utils.files.fopen(cert) as rfh:
  524                 return OpenSSL.crypto.load_certificate(
  525                     OpenSSL.crypto.FILETYPE_PEM, rfh.read()
  526                 )
  527         except Exception:  # pylint: disable=broad-except
  528             log.exception("Failed to read cert from path %s", cert)
  529             return None
  530     else:
  531         if not hasattr(cert, "get_notAfter"):
  532             log.error("%s is not a valid cert path/object", cert)
  533             return None
  534         else:
  535             return cert
  536 
  537 
  538 def validate(cert, ca_name, crl_file):
  539     """
  540     .. versionadded:: Neon
  541 
  542     Validate a certificate against a given CA/CRL.
  543 
  544     cert
  545         path to the certifiate PEM file or string
  546 
  547     ca_name
  548         name of the CA
  549 
  550     crl_file
  551         full path to the CRL file
  552     """
  553     store = OpenSSL.crypto.X509Store()
  554     cert_obj = _read_cert(cert)
  555     if cert_obj is None:
  556         raise CommandExecutionError(
  557             "Failed to read cert from {}, see log for details".format(cert)
  558         )
  559     ca_dir = "{}/{}".format(cert_base_path(), ca_name)
  560     ca_cert = _read_cert("{}/{}_ca_cert.crt".format(ca_dir, ca_name))
  561     store.add_cert(ca_cert)
  562     # These flags tell OpenSSL to check the leaf as well as the
  563     # entire cert chain.
  564     X509StoreFlags = OpenSSL.crypto.X509StoreFlags
  565     store.set_flags(X509StoreFlags.CRL_CHECK | X509StoreFlags.CRL_CHECK_ALL)
  566     if crl_file is None:
  567         crl = OpenSSL.crypto.CRL()
  568     else:
  569         with salt.utils.files.fopen(crl_file) as fhr:
  570             crl = OpenSSL.crypto.load_crl(OpenSSL.crypto.FILETYPE_PEM, fhr.read())
  571     store.add_crl(crl)
  572     context = OpenSSL.crypto.X509StoreContext(store, cert_obj)
  573     ret = {}
  574     try:
  575         context.verify_certificate()
  576         ret["valid"] = True
  577     except OpenSSL.crypto.X509StoreContextError as e:
  578         ret["error"] = str(e)
  579         ret["error_cert"] = e.certificate
  580         ret["valid"] = False
  581     return ret
  582 
  583 
  584 def _get_expiration_date(cert):
  585     """
  586     Returns a datetime.datetime object
  587     """
  588     cert_obj = _read_cert(cert)
  589 
  590     if cert_obj is None:
  591         raise CommandExecutionError(
  592             "Failed to read cert from {}, see log for details".format(cert)
  593         )
  594 
  595     return datetime.strptime(
  596         salt.utils.stringutils.to_str(cert_obj.get_notAfter()), four_digit_year_fmt
  597     )
  598 
  599 
  600 def get_expiration_date(cert, date_format="%Y-%m-%d"):
  601     """
  602     .. versionadded:: 2019.2.0
  603 
  604     Get a certificate's expiration date
  605 
  606     cert
  607         Full path to the certificate
  608 
  609     date_format
  610         By default this will return the expiration date in YYYY-MM-DD format,
  611         use this to specify a different strftime format string. Note that the
  612         expiration time will be in UTC.
  613 
  614     CLI Examples:
  615 
  616     .. code-block:: bash
  617 
  618         salt '*' tls.get_expiration_date /path/to/foo.crt
  619         salt '*' tls.get_expiration_date /path/to/foo.crt date_format='%d/%m/%Y'
  620     """
  621     return _get_expiration_date(cert).strftime(date_format)
  622 
  623 
  624 def _check_onlyif_unless(onlyif, unless):
  625     ret = None
  626     retcode = __salt__["cmd.retcode"]
  627     if onlyif is not None:
  628         if not isinstance(onlyif, str):
  629             if not onlyif:
  630                 ret = {"comment": "onlyif condition is false", "result": True}
  631         elif isinstance(onlyif, str):
  632             if retcode(onlyif) != 0:
  633                 ret = {"comment": "onlyif condition is false", "result": True}
  634                 log.debug("onlyif condition is false")
  635     if unless is not None:
  636         if not isinstance(unless, str):
  637             if unless:
  638                 ret = {"comment": "unless condition is true", "result": True}
  639         elif isinstance(unless, str):
  640             if retcode(unless) == 0:
  641                 ret = {"comment": "unless condition is true", "result": True}
  642                 log.debug("unless condition is true")
  643     return ret
  644 
  645 
  646 def create_ca(
  647     ca_name,
  648     bits=2048,
  649     days=365,
  650     CN="localhost",
  651     C="US",
  652     ST="Utah",
  653     L="Salt Lake City",
  654     O="SaltStack",
  655     OU=None,
  656     emailAddress=None,
  657     fixmode=False,
  658     cacert_path=None,
  659     ca_filename=None,
  660     digest="sha256",
  661     onlyif=None,
  662     unless=None,
  663     replace=False,
  664 ):
  665     """
  666     Create a Certificate Authority (CA)
  667 
  668     ca_name
  669         name of the CA
  670     bits
  671         number of RSA key bits, default is 2048
  672     days
  673         number of days the CA will be valid, default is 365
  674     CN
  675         common name in the request, default is "localhost"
  676     C
  677         country, default is "US"
  678     ST
  679         state, default is "Utah"
  680     L
  681         locality, default is "Centerville", the city where SaltStack originated
  682     O
  683         organization, default is "SaltStack"
  684     OU
  685         organizational unit, default is None
  686     emailAddress
  687         email address for the CA owner, default is None
  688     cacert_path
  689         absolute path to ca certificates root directory
  690     ca_filename
  691         alternative filename for the CA
  692 
  693         .. versionadded:: 2015.5.3
  694 
  695     digest
  696         The message digest algorithm. Must be a string describing a digest
  697         algorithm supported by OpenSSL (by EVP_get_digestbyname, specifically).
  698         For example, "md5" or "sha1". Default: 'sha256'
  699     replace
  700         Replace this certificate even if it exists
  701 
  702         .. versionadded:: 2015.5.1
  703 
  704     Writes out a CA certificate based upon defined config values. If the file
  705     already exists, the function just returns assuming the CA certificate
  706     already exists.
  707 
  708     If the following values were set::
  709 
  710         ca.cert_base_path='/etc/pki'
  711         ca_name='koji'
  712 
  713     the resulting CA, and corresponding key, would be written in the following
  714     location with appropriate permissions::
  715 
  716         /etc/pki/koji/koji_ca_cert.crt
  717         /etc/pki/koji/koji_ca_cert.key
  718 
  719     CLI Example:
  720 
  721     .. code-block:: bash
  722 
  723         salt '*' tls.create_ca test_ca
  724     """
  725     status = _check_onlyif_unless(onlyif, unless)
  726     if status is not None:
  727         return None
  728 
  729     set_ca_path(cacert_path)
  730 
  731     if not ca_filename:
  732         ca_filename = "{}_ca_cert".format(ca_name)
  733 
  734     certp = "{}/{}/{}.crt".format(cert_base_path(), ca_name, ca_filename)
  735     ca_keyp = "{}/{}/{}.key".format(cert_base_path(), ca_name, ca_filename)
  736     if not replace and not fixmode and ca_exists(ca_name, ca_filename=ca_filename):
  737         return 'Certificate for CA named "{}" already exists'.format(ca_name)
  738 
  739     if fixmode and not os.path.exists(certp):
  740         raise ValueError("{} does not exists, can't fix".format(certp))
  741 
  742     if not os.path.exists("{}/{}".format(cert_base_path(), ca_name)):
  743         os.makedirs("{}/{}".format(cert_base_path(), ca_name))
  744 
  745     # try to reuse existing ssl key
  746     key = None
  747     if os.path.exists(ca_keyp):
  748         with salt.utils.files.fopen(ca_keyp) as fic2:
  749             # try to determine the key bits
  750             try:
  751                 key = OpenSSL.crypto.load_privatekey(
  752                     OpenSSL.crypto.FILETYPE_PEM, fic2.read()
  753                 )
  754             except OpenSSL.crypto.Error as err:
  755                 log.warning(
  756                     "Error loading existing private key"
  757                     " %s, generating a new key: %s",
  758                     ca_keyp,
  759                     err,
  760                 )
  761                 bck = "{}.unloadable.{}".format(
  762                     ca_keyp, datetime.utcnow().strftime("%Y%m%d%H%M%S")
  763                 )
  764                 log.info("Saving unloadable CA ssl key in %s", bck)
  765                 os.rename(ca_keyp, bck)
  766 
  767     if not key:
  768         key = OpenSSL.crypto.PKey()
  769         key.generate_key(OpenSSL.crypto.TYPE_RSA, bits)
  770 
  771     ca = OpenSSL.crypto.X509()
  772     ca.set_version(2)
  773     ca.set_serial_number(_new_serial(ca_name))
  774     ca.get_subject().C = C
  775     ca.get_subject().ST = ST
  776     ca.get_subject().L = L
  777     ca.get_subject().O = O
  778     if OU:
  779         ca.get_subject().OU = OU
  780     ca.get_subject().CN = CN
  781     if emailAddress:
  782         ca.get_subject().emailAddress = emailAddress
  783 
  784     ca.gmtime_adj_notBefore(0)
  785     ca.gmtime_adj_notAfter(int(days) * 24 * 60 * 60)
  786     ca.set_issuer(ca.get_subject())
  787     ca.set_pubkey(key)
  788 
  789     if X509_EXT_ENABLED:
  790         ca.add_extensions(
  791             [
  792                 OpenSSL.crypto.X509Extension(
  793                     b"basicConstraints", True, b"CA:TRUE, pathlen:0"
  794                 ),
  795                 OpenSSL.crypto.X509Extension(
  796                     b"keyUsage", True, b"keyCertSign, cRLSign"
  797                 ),
  798                 OpenSSL.crypto.X509Extension(
  799                     b"subjectKeyIdentifier", False, b"hash", subject=ca
  800                 ),
  801             ]
  802         )
  803 
  804         ca.add_extensions(
  805             [
  806                 OpenSSL.crypto.X509Extension(
  807                     b"authorityKeyIdentifier",
  808                     False,
  809                     b"issuer:always,keyid:always",
  810                     issuer=ca,
  811                 )
  812             ]
  813         )
  814     ca.sign(key, salt.utils.stringutils.to_str(digest))
  815 
  816     # always backup existing keys in case
  817     keycontent = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key)
  818     write_key = True
  819     if os.path.exists(ca_keyp):
  820         bck = "{}.{}".format(ca_keyp, datetime.utcnow().strftime("%Y%m%d%H%M%S"))
  821         with salt.utils.files.fopen(ca_keyp) as fic:
  822             old_key = salt.utils.stringutils.to_unicode(fic.read()).strip()
  823             if old_key.strip() == keycontent.strip():
  824                 write_key = False
  825             else:
  826                 log.info("Saving old CA ssl key in %s", bck)
  827                 fp = os.open(bck, os.O_CREAT | os.O_RDWR, 0o600)
  828                 with salt.utils.files.fopen(fp, "w") as bckf:
  829                     bckf.write(old_key)
  830     if write_key:
  831         fp = os.open(ca_keyp, os.O_CREAT | os.O_RDWR, 0o600)
  832         with salt.utils.files.fopen(fp, "wb") as ca_key:
  833             ca_key.write(salt.utils.stringutils.to_bytes(keycontent))
  834 
  835     with salt.utils.files.fopen(certp, "wb") as ca_crt:
  836         ca_crt.write(
  837             salt.utils.stringutils.to_bytes(
  838                 OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, ca)
  839             )
  840         )
  841 
  842     _write_cert_to_database(ca_name, ca)
  843 
  844     ret = ('Created Private Key: "{}/{}/{}.key." ').format(
  845         cert_base_path(), ca_name, ca_filename
  846     )
  847     ret += ('Created CA "{0}": "{1}/{0}/{2}.crt."').format(
  848         ca_name, cert_base_path(), ca_filename
  849     )
  850 
  851     return ret
  852 
  853 
  854 def get_extensions(cert_type):
  855     """
  856     Fetch X509 and CSR extension definitions from tls:extensions:
  857     (common|server|client) or set them to standard defaults.
  858 
  859     .. versionadded:: 2015.8.0
  860 
  861     cert_type:
  862         The type of certificate such as ``server`` or ``client``.
  863 
  864     CLI Example:
  865 
  866     .. code-block:: bash
  867 
  868         salt '*' tls.get_extensions client
  869 
  870     """
  871 
  872     assert X509_EXT_ENABLED, (
  873         "X509 extensions are not supported in "
  874         "pyOpenSSL prior to version 0.15.1. Your "
  875         "version: {}".format(OpenSSL_version)
  876     )
  877 
  878     ext = {}
  879     if cert_type == "":
  880         log.error(
  881             "cert_type set to empty in tls_ca.get_extensions(); "
  882             "defaulting to ``server``"
  883         )
  884         cert_type = "server"
  885 
  886     try:
  887         ext["common"] = __salt__["pillar.get"]("tls.extensions:common", False)
  888     except NameError as err:
  889         log.debug(err)
  890 
  891     if not ext["common"] or ext["common"] == "":
  892         ext["common"] = {
  893             "csr": {"basicConstraints": "CA:FALSE"},
  894             "cert": {
  895                 "authorityKeyIdentifier": "keyid,issuer:always",
  896                 "subjectKeyIdentifier": "hash",
  897             },
  898         }
  899 
  900     try:
  901         ext["server"] = __salt__["pillar.get"]("tls.extensions:server", False)
  902     except NameError as err:
  903         log.debug(err)
  904 
  905     if not ext["server"] or ext["server"] == "":
  906         ext["server"] = {
  907             "csr": {
  908                 "extendedKeyUsage": "serverAuth",
  909                 "keyUsage": "digitalSignature, keyEncipherment",
  910             },
  911             "cert": {},
  912         }
  913 
  914     try:
  915         ext["client"] = __salt__["pillar.get"]("tls.extensions:client", False)
  916     except NameError as err:
  917         log.debug(err)
  918 
  919     if not ext["client"] or ext["client"] == "":
  920         ext["client"] = {
  921             "csr": {
  922                 "extendedKeyUsage": "clientAuth",
  923                 "keyUsage": "nonRepudiation, digitalSignature, keyEncipherment",
  924             },
  925             "cert": {},
  926         }
  927 
  928     # possible user-defined profile or a typo
  929     if cert_type not in ext:
  930         try:
  931             ext[cert_type] = __salt__["pillar.get"](
  932                 "tls.extensions:{}".format(cert_type)
  933             )
  934         except NameError as e:
  935             log.debug(
  936                 "pillar, tls:extensions:%s not available or "
  937                 "not operating in a salt context\n%s",
  938                 cert_type,
  939                 e,
  940             )
  941 
  942     retval = ext["common"]
  943 
  944     for Use in retval:
  945         retval[Use].update(ext[cert_type][Use])
  946 
  947     return retval
  948 
  949 
  950 def create_csr(
  951     ca_name,
  952     bits=2048,
  953     CN="localhost",
  954     C="US",
  955     ST="Utah",
  956     L="Salt Lake City",
  957     O="SaltStack",
  958     OU=None,
  959     emailAddress=None,
  960     subjectAltName=None,
  961     cacert_path=None,
  962     ca_filename=None,
  963     csr_path=None,
  964     csr_filename=None,
  965     digest="sha256",
  966     type_ext=False,
  967     cert_type="server",
  968     replace=False,
  969 ):
  970     """
  971     Create a Certificate Signing Request (CSR) for a
  972     particular Certificate Authority (CA)
  973 
  974     ca_name
  975         name of the CA
  976     bits
  977         number of RSA key bits, default is 2048
  978     CN
  979         common name in the request, default is "localhost"
  980     C
  981         country, default is "US"
  982     ST
  983         state, default is "Utah"
  984     L
  985         locality, default is "Centerville", the city where SaltStack originated
  986     O
  987         organization, default is "SaltStack"
  988         NOTE: Must the same as CA certificate or an error will be raised
  989     OU
  990         organizational unit, default is None
  991     emailAddress
  992         email address for the request, default is None
  993     subjectAltName
  994         valid subjectAltNames in full form, e.g. to add DNS entry you would call
  995         this function with this value:
  996 
  997         examples: ['DNS:somednsname.com',
  998                 'DNS:1.2.3.4',
  999                 'IP:1.2.3.4',
 1000                 'IP:2001:4801:7821:77:be76:4eff:fe11:e51',
 1001                 'email:me@i.like.pie.com']
 1002 
 1003     .. note::
 1004         some libraries do not properly query IP: prefixes, instead looking
 1005         for the given req. source with a DNS: prefix. To be thorough, you
 1006         may want to include both DNS: and IP: entries if you are using
 1007         subjectAltNames for destinations for your TLS connections.
 1008         e.g.:
 1009         requests to https://1.2.3.4 will fail from python's
 1010         requests library w/out the second entry in the above list
 1011 
 1012     .. versionadded:: 2015.8.0
 1013 
 1014     cert_type
 1015         Specify the general certificate type. Can be either `server` or
 1016         `client`. Indicates the set of common extensions added to the CSR.
 1017 
 1018         .. code-block:: cfg
 1019 
 1020             server: {
 1021                'basicConstraints': 'CA:FALSE',
 1022                'extendedKeyUsage': 'serverAuth',
 1023                'keyUsage': 'digitalSignature, keyEncipherment'
 1024             }
 1025 
 1026             client: {
 1027                'basicConstraints': 'CA:FALSE',
 1028                'extendedKeyUsage': 'clientAuth',
 1029                'keyUsage': 'nonRepudiation, digitalSignature, keyEncipherment'
 1030             }
 1031 
 1032     type_ext
 1033         boolean.  Whether or not to extend the filename with CN_[cert_type]
 1034         This can be useful if a server and client certificate are needed for
 1035         the same CN. Defaults to False to avoid introducing an unexpected file
 1036         naming pattern
 1037 
 1038         The files normally named some_subject_CN.csr and some_subject_CN.key
 1039         will then be saved
 1040 
 1041     replace
 1042         Replace this signing request even if it exists
 1043 
 1044         .. versionadded:: 2015.5.1
 1045 
 1046     Writes out a Certificate Signing Request (CSR) If the file already
 1047     exists, the function just returns assuming the CSR already exists.
 1048 
 1049     If the following values were set::
 1050 
 1051         ca.cert_base_path='/etc/pki'
 1052         ca_name='koji'
 1053         CN='test.egavas.org'
 1054 
 1055     the resulting CSR, and corresponding key, would be written in the
 1056     following location with appropriate permissions::
 1057 
 1058         /etc/pki/koji/certs/test.egavas.org.csr
 1059         /etc/pki/koji/certs/test.egavas.org.key
 1060 
 1061     CLI Example:
 1062 
 1063     .. code-block:: bash
 1064 
 1065         salt '*' tls.create_csr test
 1066     """
 1067     set_ca_path(cacert_path)
 1068 
 1069     if not ca_filename:
 1070         ca_filename = "{}_ca_cert".format(ca_name)
 1071 
 1072     if not ca_exists(ca_name, ca_filename=ca_filename):
 1073         return (
 1074             'Certificate for CA named "{}" does not exist, please create ' "it first."
 1075         ).format(ca_name)
 1076 
 1077     if not csr_path:
 1078         csr_path = "{}/{}/certs/".format(cert_base_path(), ca_name)
 1079 
 1080     if not os.path.exists(csr_path):
 1081         os.makedirs(csr_path)
 1082 
 1083     CN_ext = "_{}".format(cert_type) if type_ext else ""
 1084 
 1085     if not csr_filename:
 1086         csr_filename = "{}{}".format(CN, CN_ext)
 1087 
 1088     csr_f = "{}/{}.csr".format(csr_path, csr_filename)
 1089 
 1090     if not replace and os.path.exists(csr_f):
 1091         return 'Certificate Request "{}" already exists'.format(csr_f)
 1092 
 1093     key = OpenSSL.crypto.PKey()
 1094     key.generate_key(OpenSSL.crypto.TYPE_RSA, bits)
 1095 
 1096     req = OpenSSL.crypto.X509Req()
 1097 
 1098     req.get_subject().C = C
 1099     req.get_subject().ST = ST
 1100     req.get_subject().L = L
 1101     req.get_subject().O = O
 1102     if OU:
 1103         req.get_subject().OU = OU
 1104     req.get_subject().CN = CN
 1105     if emailAddress:
 1106         req.get_subject().emailAddress = emailAddress
 1107 
 1108     try:
 1109         extensions = get_extensions(cert_type)["csr"]
 1110 
 1111         extension_adds = []
 1112 
 1113         for ext, value in extensions.items():
 1114             if isinstance(value, str):
 1115                 value = salt.utils.stringutils.to_bytes(value)
 1116             extension_adds.append(
 1117                 OpenSSL.crypto.X509Extension(
 1118                     salt.utils.stringutils.to_bytes(ext), False, value
 1119                 )
 1120             )
 1121     except AssertionError as err:
 1122         log.error(err)
 1123         extensions = []
 1124 
 1125     if subjectAltName:
 1126         if X509_EXT_ENABLED:
 1127             if isinstance(subjectAltName, str):
 1128                 subjectAltName = [subjectAltName]
 1129 
 1130             extension_adds.append(
 1131                 OpenSSL.crypto.X509Extension(
 1132                     b"subjectAltName",
 1133                     False,
 1134                     b", ".join(salt.utils.data.encode(subjectAltName)),
 1135                 )
 1136             )
 1137         else:
 1138             raise ValueError(
 1139                 "subjectAltName cannot be set as X509 "
 1140                 "extensions are not supported in pyOpenSSL "
 1141                 "prior to version 0.15.1. Your "
 1142                 "version: {}.".format(OpenSSL_version)
 1143             )
 1144 
 1145     if X509_EXT_ENABLED:
 1146         req.add_extensions(extension_adds)
 1147 
 1148     req.set_pubkey(key)
 1149     req.sign(key, salt.utils.stringutils.to_str(digest))
 1150 
 1151     # Write private key and request
 1152     priv_keyp = "{}/{}.key".format(csr_path, csr_filename)
 1153     fp = os.open(priv_keyp, os.O_CREAT | os.O_RDWR, 0o600)
 1154     with salt.utils.files.fopen(fp, "wb+") as priv_key:
 1155         priv_key.write(
 1156             salt.utils.stringutils.to_bytes(
 1157                 OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key)
 1158             )
 1159         )
 1160 
 1161     with salt.utils.files.fopen(csr_f, "wb+") as csr:
 1162         csr.write(
 1163             salt.utils.stringutils.to_bytes(
 1164                 OpenSSL.crypto.dump_certificate_request(
 1165                     OpenSSL.crypto.FILETYPE_PEM, req
 1166                 )
 1167             )
 1168         )
 1169 
 1170     ret = 'Created Private Key: "{}{}.key." '.format(csr_path, csr_filename)
 1171     ret += 'Created CSR for "{}": "{}{}.csr."'.format(CN, csr_path, csr_filename)
 1172 
 1173     return ret
 1174 
 1175 
 1176 def create_self_signed_cert(
 1177     tls_dir="tls",
 1178     bits=2048,
 1179     days=365,
 1180     CN="localhost",
 1181     C="US",
 1182     ST="Utah",
 1183     L="Salt Lake City",
 1184     O="SaltStack",
 1185     OU=None,
 1186     emailAddress=None,
 1187     cacert_path=None,
 1188     cert_filename=None,
 1189     digest="sha256",
 1190     replace=False,
 1191 ):
 1192     """
 1193     Create a Self-Signed Certificate (CERT)
 1194 
 1195     tls_dir
 1196         location appended to the ca.cert_base_path, default is 'tls'
 1197     bits
 1198         number of RSA key bits, default is 2048
 1199     CN
 1200         common name in the request, default is "localhost"
 1201     C
 1202         country, default is "US"
 1203     ST
 1204         state, default is "Utah"
 1205     L
 1206         locality, default is "Centerville", the city where SaltStack originated
 1207     O
 1208         organization, default is "SaltStack"
 1209         NOTE: Must the same as CA certificate or an error will be raised
 1210     OU
 1211         organizational unit, default is None
 1212     emailAddress
 1213         email address for the request, default is None
 1214     cacert_path
 1215         absolute path to ca certificates root directory
 1216     digest
 1217         The message digest algorithm. Must be a string describing a digest
 1218         algorithm supported by OpenSSL (by EVP_get_digestbyname, specifically).
 1219         For example, "md5" or "sha1". Default: 'sha256'
 1220     replace
 1221         Replace this certificate even if it exists
 1222 
 1223         .. versionadded:: 2015.5.1
 1224 
 1225     Writes out a Self-Signed Certificate (CERT). If the file already
 1226     exists, the function just returns.
 1227 
 1228     If the following values were set::
 1229 
 1230         ca.cert_base_path='/etc/pki'
 1231         tls_dir='koji'
 1232         CN='test.egavas.org'
 1233 
 1234     the resulting CERT, and corresponding key, would be written in the
 1235     following location with appropriate permissions::
 1236 
 1237         /etc/pki/koji/certs/test.egavas.org.crt
 1238         /etc/pki/koji/certs/test.egavas.org.key
 1239 
 1240     CLI Example:
 1241 
 1242     .. code-block:: bash
 1243 
 1244         salt '*' tls.create_self_signed_cert
 1245 
 1246     Passing options from the command line:
 1247 
 1248     .. code-block:: bash
 1249 
 1250         salt 'minion' tls.create_self_signed_cert CN='test.mysite.org'
 1251     """
 1252     set_ca_path(cacert_path)
 1253 
 1254     if not os.path.exists("{}/{}/certs/".format(cert_base_path(), tls_dir)):
 1255         os.makedirs("{}/{}/certs/".format(cert_base_path(), tls_dir))
 1256 
 1257     if not cert_filename:
 1258         cert_filename = CN
 1259 
 1260     if not replace and os.path.exists(
 1261         "{}/{}/certs/{}.crt".format(cert_base_path(), tls_dir, cert_filename)
 1262     ):
 1263         return 'Certificate "{}" already exists'.format(cert_filename)
 1264 
 1265     key = OpenSSL.crypto.PKey()
 1266     key.generate_key(OpenSSL.crypto.TYPE_RSA, bits)
 1267 
 1268     # create certificate
 1269     cert = OpenSSL.crypto.X509()
 1270     cert.set_version(2)
 1271 
 1272     cert.gmtime_adj_notBefore(0)
 1273     cert.gmtime_adj_notAfter(int(days) * 24 * 60 * 60)
 1274 
 1275     cert.get_subject().C = C
 1276     cert.get_subject().ST = ST
 1277     cert.get_subject().L = L
 1278     cert.get_subject().O = O
 1279     if OU:
 1280         cert.get_subject().OU = OU
 1281     cert.get_subject().CN = CN
 1282     if emailAddress:
 1283         cert.get_subject().emailAddress = emailAddress
 1284 
 1285     cert.set_serial_number(_new_serial(tls_dir))
 1286     cert.set_issuer(cert.get_subject())
 1287     cert.set_pubkey(key)
 1288     cert.sign(key, salt.utils.stringutils.to_str(digest))
 1289 
 1290     # Write private key and cert
 1291     priv_key_path = "{}/{}/certs/{}.key".format(
 1292         cert_base_path(), tls_dir, cert_filename
 1293     )
 1294     fp = os.open(priv_key_path, os.O_CREAT | os.O_RDWR, 0o600)
 1295     with salt.utils.files.fopen(fp, "wb+") as priv_key:
 1296         priv_key.write(
 1297             salt.utils.stringutils.to_bytes(
 1298                 OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key)
 1299             )
 1300         )
 1301 
 1302     crt_path = "{}/{}/certs/{}.crt".format(cert_base_path(), tls_dir, cert_filename)
 1303     with salt.utils.files.fopen(crt_path, "wb+") as crt:
 1304         crt.write(
 1305             salt.utils.stringutils.to_bytes(
 1306                 OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)
 1307             )
 1308         )
 1309 
 1310     _write_cert_to_database(tls_dir, cert)
 1311 
 1312     ret = 'Created Private Key: "{}/{}/certs/{}.key." '.format(
 1313         cert_base_path(), tls_dir, cert_filename
 1314     )
 1315     ret += 'Created Certificate: "{}/{}/certs/{}.crt."'.format(
 1316         cert_base_path(), tls_dir, cert_filename
 1317     )
 1318 
 1319     return ret
 1320 
 1321 
 1322 def create_ca_signed_cert(
 1323     ca_name,
 1324     CN,
 1325     days=365,
 1326     cacert_path=None,
 1327     ca_filename=None,
 1328     cert_path=None,
 1329     cert_filename=None,
 1330     digest="sha256",
 1331     cert_type=None,
 1332     type_ext=False,
 1333     replace=False,
 1334 ):
 1335     """
 1336     Create a Certificate (CERT) signed by a named Certificate Authority (CA)
 1337 
 1338     If the certificate file already exists, the function just returns assuming
 1339     the CERT already exists.
 1340 
 1341     The CN *must* match an existing CSR generated by create_csr. If it
 1342     does not, this method does nothing.
 1343 
 1344     ca_name
 1345         name of the CA
 1346     CN
 1347         common name matching the certificate signing request
 1348     days
 1349         number of days certificate is valid, default is 365 (1 year)
 1350 
 1351     cacert_path
 1352         absolute path to ca certificates root directory
 1353 
 1354     ca_filename
 1355         alternative filename for the CA
 1356 
 1357         .. versionadded:: 2015.5.3
 1358 
 1359 
 1360     cert_path
 1361         full path to the certificates directory
 1362 
 1363     cert_filename
 1364         alternative filename for the certificate, useful when using special
 1365         characters in the CN. If this option is set it will override
 1366         the certificate filename output effects of ``cert_type``.
 1367         ``type_ext`` will be completely overridden.
 1368 
 1369         .. versionadded:: 2015.5.3
 1370 
 1371 
 1372     digest
 1373         The message digest algorithm. Must be a string describing a digest
 1374         algorithm supported by OpenSSL (by EVP_get_digestbyname, specifically).
 1375         For example, "md5" or "sha1". Default: 'sha256'
 1376     replace
 1377         Replace this certificate even if it exists
 1378 
 1379         .. versionadded:: 2015.5.1
 1380 
 1381     cert_type
 1382         string. Either 'server' or 'client' (see create_csr() for details).
 1383 
 1384         If create_csr(type_ext=True) this function **must** be called with the
 1385         same cert_type so it can find the CSR file.
 1386 
 1387     .. note::
 1388         create_csr() defaults to cert_type='server'; therefore, if it was also
 1389         called with type_ext, cert_type becomes a required argument for
 1390         create_ca_signed_cert()
 1391 
 1392     type_ext
 1393         bool. If set True, use ``cert_type`` as an extension to the CN when
 1394         formatting the filename.
 1395 
 1396         e.g.: some_subject_CN_server.crt or some_subject_CN_client.crt
 1397 
 1398         This facilitates the context where both types are required for the same
 1399         subject
 1400 
 1401         If ``cert_filename`` is `not None`, setting ``type_ext`` has no
 1402         effect
 1403 
 1404     If the following values were set:
 1405 
 1406     .. code-block:: text
 1407 
 1408         ca.cert_base_path='/etc/pki'
 1409         ca_name='koji'
 1410         CN='test.egavas.org'
 1411 
 1412     the resulting signed certificate would be written in the following
 1413     location:
 1414 
 1415     .. code-block:: text
 1416 
 1417         /etc/pki/koji/certs/test.egavas.org.crt
 1418 
 1419     CLI Example:
 1420 
 1421     .. code-block:: bash
 1422 
 1423         salt '*' tls.create_ca_signed_cert test localhost
 1424     """
 1425     ret = {}
 1426 
 1427     set_ca_path(cacert_path)
 1428 
 1429     if not ca_filename:
 1430         ca_filename = "{}_ca_cert".format(ca_name)
 1431 
 1432     if not cert_path:
 1433         cert_path = "{}/{}/certs".format(cert_base_path(), ca_name)
 1434 
 1435     if type_ext:
 1436         if not cert_type:
 1437             log.error(
 1438                 "type_ext = True but cert_type is unset. " "Certificate not written."
 1439             )
 1440             return ret
 1441         elif cert_type:
 1442             CN_ext = "_{}".format(cert_type)
 1443     else:
 1444         CN_ext = ""
 1445 
 1446     csr_filename = "{}{}".format(CN, CN_ext)
 1447 
 1448     if not cert_filename:
 1449         cert_filename = "{}{}".format(CN, CN_ext)
 1450 
 1451     if not replace and os.path.exists(
 1452         os.path.join(
 1453             os.path.sep.join(
 1454                 "{}/{}/certs/{}.crt".format(
 1455                     cert_base_path(), ca_name, cert_filename
 1456                 ).split("/")
 1457             )
 1458         )
 1459     ):
 1460         return 'Certificate "{}" already exists'.format(cert_filename)
 1461 
 1462     try:
 1463         maybe_fix_ssl_version(ca_name, cacert_path=cacert_path, ca_filename=ca_filename)
 1464         with salt.utils.files.fopen(
 1465             "{}/{}/{}.crt".format(cert_base_path(), ca_name, ca_filename)
 1466         ) as fhr:
 1467             ca_cert = OpenSSL.crypto.load_certificate(
 1468                 OpenSSL.crypto.FILETYPE_PEM, fhr.read()
 1469             )
 1470         with salt.utils.files.fopen(
 1471             "{}/{}/{}.key".format(cert_base_path(), ca_name, ca_filename)
 1472         ) as fhr:
 1473             ca_key = OpenSSL.crypto.load_privatekey(
 1474                 OpenSSL.crypto.FILETYPE_PEM, fhr.read()
 1475             )
 1476     except OSError:
 1477         ret["retcode"] = 1
 1478         ret["comment"] = 'There is no CA named "{}"'.format(ca_name)
 1479         return ret
 1480 
 1481     try:
 1482         csr_path = "{}/{}.csr".format(cert_path, csr_filename)
 1483         with salt.utils.files.fopen(csr_path) as fhr:
 1484             req = OpenSSL.crypto.load_certificate_request(
 1485                 OpenSSL.crypto.FILETYPE_PEM, fhr.read()
 1486             )
 1487     except OSError:
 1488         ret["retcode"] = 1
 1489         ret["comment"] = 'There is no CSR that matches the CN "{}"'.format(
 1490             cert_filename
 1491         )
 1492         return ret
 1493 
 1494     exts = []
 1495     try:
 1496         exts.extend(req.get_extensions())
 1497     except AttributeError:
 1498         try:
 1499             # see: http://bazaar.launchpad.net/~exarkun/pyopenssl/master/revision/189
 1500             # support is there from quite a long time, but without API
 1501             # so we mimic the newly get_extensions method present in ultra
 1502             # recent pyopenssl distros
 1503             log.info(
 1504                 "req.get_extensions() not supported in pyOpenSSL versions "
 1505                 "prior to 0.15. Processing extensions internally. "
 1506                 "Your version: %s",
 1507                 OpenSSL_version,
 1508             )
 1509 
 1510             native_exts_obj = OpenSSL._util.lib.X509_REQ_get_extensions(req._req)
 1511             for i in range(OpenSSL._util.lib.sk_X509_EXTENSION_num(native_exts_obj)):
 1512                 ext = OpenSSL.crypto.X509Extension.__new__(OpenSSL.crypto.X509Extension)
 1513                 ext._extension = OpenSSL._util.lib.sk_X509_EXTENSION_value(
 1514                     native_exts_obj, i
 1515                 )
 1516                 exts.append(ext)
 1517         except Exception:  # pylint: disable=broad-except
 1518             log.error(
 1519                 "X509 extensions are unsupported in pyOpenSSL "
 1520                 "versions prior to 0.14. Upgrade required to "
 1521                 "use extensions. Current version: %s",
 1522                 OpenSSL_version,
 1523             )
 1524 
 1525     cert = OpenSSL.crypto.X509()
 1526     cert.set_version(2)
 1527     cert.set_subject(req.get_subject())
 1528     cert.gmtime_adj_notBefore(0)
 1529     cert.gmtime_adj_notAfter(int(days) * 24 * 60 * 60)
 1530     cert.set_serial_number(_new_serial(ca_name))
 1531     cert.set_issuer(ca_cert.get_subject())
 1532     cert.set_pubkey(req.get_pubkey())
 1533 
 1534     cert.add_extensions(exts)
 1535 
 1536     cert.sign(ca_key, salt.utils.stringutils.to_str(digest))
 1537 
 1538     cert_full_path = "{}/{}.crt".format(cert_path, cert_filename)
 1539 
 1540     with salt.utils.files.fopen(cert_full_path, "wb+") as crt:
 1541         crt.write(
 1542             salt.utils.stringutils.to_bytes(
 1543                 OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)
 1544             )
 1545         )
 1546 
 1547     _write_cert_to_database(ca_name, cert)
 1548 
 1549     return 'Created Certificate for "{}": "{}/{}.crt"'.format(
 1550         CN, cert_path, cert_filename
 1551     )
 1552 
 1553 
 1554 def create_pkcs12(ca_name, CN, passphrase="", cacert_path=None, replace=False):
 1555     """
 1556     Create a PKCS#12 browser certificate for a particular Certificate (CN)
 1557 
 1558     ca_name
 1559         name of the CA
 1560     CN
 1561         common name matching the certificate signing request
 1562     passphrase
 1563         used to unlock the PKCS#12 certificate when loaded into the browser
 1564     cacert_path
 1565         absolute path to ca certificates root directory
 1566     replace
 1567         Replace this certificate even if it exists
 1568 
 1569         .. versionadded:: 2015.5.1
 1570 
 1571     If the following values were set::
 1572 
 1573         ca.cert_base_path='/etc/pki'
 1574         ca_name='koji'
 1575         CN='test.egavas.org'
 1576 
 1577     the resulting signed certificate would be written in the
 1578     following location::
 1579 
 1580         /etc/pki/koji/certs/test.egavas.org.p12
 1581 
 1582     CLI Example:
 1583 
 1584     .. code-block:: bash
 1585 
 1586         salt '*' tls.create_pkcs12 test localhost
 1587     """
 1588     set_ca_path(cacert_path)
 1589     if not replace and os.path.exists(
 1590         "{}/{}/certs/{}.p12".format(cert_base_path(), ca_name, CN)
 1591     ):
 1592         return 'Certificate "{}" already exists'.format(CN)
 1593 
 1594     try:
 1595         with salt.utils.files.fopen(
 1596             "{0}/{1}/{1}_ca_cert.crt".format(cert_base_path(), ca_name)
 1597         ) as fhr:
 1598             ca_cert = OpenSSL.crypto.load_certificate(
 1599                 OpenSSL.crypto.FILETYPE_PEM, fhr.read()
 1600             )
 1601     except OSError:
 1602         return 'There is no CA named "{}"'.format(ca_name)
 1603 
 1604     try:
 1605         with salt.utils.files.fopen(
 1606             "{}/{}/certs/{}.crt".format(cert_base_path(), ca_name, CN)
 1607         ) as fhr:
 1608             cert = OpenSSL.crypto.load_certificate(
 1609                 OpenSSL.crypto.FILETYPE_PEM, fhr.read()
 1610             )
 1611         with salt.utils.files.fopen(
 1612             "{}/{}/certs/{}.key".format(cert_base_path(), ca_name, CN)
 1613         ) as fhr:
 1614             key = OpenSSL.crypto.load_privatekey(
 1615                 OpenSSL.crypto.FILETYPE_PEM, fhr.read()
 1616             )
 1617     except OSError:
 1618         return 'There is no certificate that matches the CN "{}"'.format(CN)
 1619 
 1620     pkcs12 = OpenSSL.crypto.PKCS12()
 1621 
 1622     pkcs12.set_certificate(cert)
 1623     pkcs12.set_ca_certificates([ca_cert])
 1624     pkcs12.set_privatekey(key)
 1625 
 1626     with salt.utils.files.fopen(
 1627         "{}/{}/certs/{}.p12".format(cert_base_path(), ca_name, CN), "wb"
 1628     ) as ofile:
 1629         ofile.write(
 1630             pkcs12.export(passphrase=salt.utils.stringutils.to_bytes(passphrase))
 1631         )
 1632 
 1633     return ('Created PKCS#12 Certificate for "{0}": ' '"{1}/{2}/certs/{0}.p12"').format(
 1634         CN, cert_base_path(), ca_name,
 1635     )
 1636 
 1637 
 1638 def cert_info(cert, digest="sha256"):
 1639     """
 1640     Return information for a particular certificate
 1641 
 1642     cert
 1643         path to the certifiate PEM file or string
 1644 
 1645         .. versionchanged:: 2018.3.4
 1646 
 1647     digest
 1648         what digest to use for fingerprinting
 1649 
 1650     CLI Example:
 1651 
 1652     .. code-block:: bash
 1653 
 1654         salt '*' tls.cert_info /dir/for/certs/cert.pem
 1655 
 1656     """
 1657     # format that OpenSSL returns dates in
 1658     date_fmt = "%Y%m%d%H%M%SZ"
 1659     if "-----BEGIN" not in cert:
 1660         with salt.utils.files.fopen(cert) as cert_file:
 1661             cert = cert_file.read()
 1662     cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)
 1663 
 1664     issuer = {}
 1665     for key, value in cert.get_issuer().get_components():
 1666         if isinstance(key, bytes):
 1667             key = salt.utils.stringutils.to_unicode(key)
 1668         if isinstance(value, bytes):
 1669             value = salt.utils.stringutils.to_unicode(value)
 1670         issuer[key] = value
 1671 
 1672     subject = {}
 1673     for key, value in cert.get_subject().get_components():
 1674         if isinstance(key, bytes):
 1675             key = salt.utils.stringutils.to_unicode(key)
 1676         if isinstance(value, bytes):
 1677             value = salt.utils.stringutils.to_unicode(value)
 1678         subject[key] = value
 1679 
 1680     ret = {
 1681         "fingerprint": salt.utils.stringutils.to_unicode(
 1682             cert.digest(salt.utils.stringutils.to_str(digest))
 1683         ),
 1684         "subject": subject,
 1685         "issuer": issuer,
 1686         "serial_number": cert.get_serial_number(),
 1687         "not_before": calendar.timegm(
 1688             time.strptime(
 1689                 str(cert.get_notBefore().decode(__salt_system_encoding__)), date_fmt
 1690             )
 1691         ),
 1692         "not_after": calendar.timegm(
 1693             time.strptime(
 1694                 cert.get_notAfter().decode(__salt_system_encoding__), date_fmt
 1695             )
 1696         ),
 1697     }
 1698 
 1699     # add additional info if your version of pyOpenSSL supports it
 1700     if hasattr(cert, "get_extension_count"):
 1701         ret["extensions"] = {}
 1702         for i in range(cert.get_extension_count()):
 1703             try:
 1704                 ext = cert.get_extension(i)
 1705                 key = salt.utils.stringutils.to_unicode(ext.get_short_name())
 1706                 ret["extensions"][key] = str(ext).strip()
 1707             except AttributeError:
 1708                 continue
 1709 
 1710     if "subjectAltName" in ret.get("extensions", {}):
 1711         valid_entries = ("DNS", "IP Address")
 1712         valid_names = set()
 1713         for name in str(ret["extensions"]["subjectAltName"]).split(", "):
 1714             entry, name = name.split(":", 1)
 1715             if entry not in valid_entries:
 1716                 log.error(
 1717                     "Cert %s has an entry (%s) which does not start " "with %s",
 1718                     ret["subject"],
 1719                     name,
 1720                     "/".join(valid_entries),
 1721                 )
 1722             else:
 1723                 valid_names.add(name)
 1724         ret["subject_alt_names"] = list(valid_names)
 1725 
 1726     if hasattr(cert, "get_signature_algorithm"):
 1727         try:
 1728             value = cert.get_signature_algorithm()
 1729             if isinstance(value, bytes):
 1730                 value = salt.utils.stringutils.to_unicode(value)
 1731             ret["signature_algorithm"] = value
 1732         except AttributeError:
 1733             # On py3 at least
 1734             # AttributeError: cdata 'X509 *' points to an opaque type: cannot read fields
 1735             pass
 1736 
 1737     return ret
 1738 
 1739 
 1740 def create_empty_crl(
 1741     ca_name, cacert_path=None, ca_filename=None, crl_file=None, digest="sha256"
 1742 ):
 1743     """
 1744     Create an empty Certificate Revocation List.
 1745 
 1746     .. versionadded:: 2015.8.0
 1747 
 1748     ca_name
 1749         name of the CA
 1750     cacert_path
 1751         absolute path to ca certificates root directory
 1752     ca_filename
 1753         alternative filename for the CA
 1754 
 1755         .. versionadded:: 2015.5.3
 1756 
 1757     crl_file
 1758         full path to the CRL file
 1759 
 1760     digest
 1761         The message digest algorithm. Must be a string describing a digest
 1762         algorithm supported by OpenSSL (by EVP_get_digestbyname, specifically).
 1763         For example, "md5" or "sha1". Default: 'sha256'
 1764 
 1765     CLI Example:
 1766 
 1767     .. code-block:: bash
 1768 
 1769         salt '*' tls.create_empty_crl ca_name='koji' \
 1770                 ca_filename='ca' \
 1771                 crl_file='/etc/openvpn/team1/crl.pem'
 1772     """
 1773 
 1774     set_ca_path(cacert_path)
 1775 
 1776     if not ca_filename:
 1777         ca_filename = "{}_ca_cert".format(ca_name)
 1778 
 1779     if not crl_file:
 1780         crl_file = "{}/{}/crl.pem".format(_cert_base_path(), ca_name)
 1781 
 1782     if os.path.exists("{}".format(crl_file)):
 1783         return 'CRL "{}" already exists'.format(crl_file)
 1784 
 1785     try:
 1786         with salt.utils.files.fopen(
 1787             "{}/{}/{}.crt".format(cert_base_path(), ca_name, ca_filename)
 1788         ) as fp_:
 1789             ca_cert = OpenSSL.crypto.load_certificate(
 1790                 OpenSSL.crypto.FILETYPE_PEM, fp_.read()
 1791             )
 1792         with salt.utils.files.fopen(
 1793             "{}/{}/{}.key".format(cert_base_path(), ca_name, ca_filename)
 1794         ) as fp_:
 1795             ca_key = OpenSSL.crypto.load_privatekey(
 1796                 OpenSSL.crypto.FILETYPE_PEM, fp_.read()
 1797             )
 1798     except OSError:
 1799         return 'There is no CA named "{}"'.format(ca_name)
 1800 
 1801     crl = OpenSSL.crypto.CRL()
 1802     crl_text = crl.export(
 1803         ca_cert, ca_key, digest=salt.utils.stringutils.to_bytes(digest),
 1804     )
 1805 
 1806     with salt.utils.files.fopen(crl_file, "w") as f:
 1807         f.write(salt.utils.stringutils.to_str(crl_text))
 1808 
 1809     return 'Created an empty CRL: "{}"'.format(crl_file)
 1810 
 1811 
 1812 def revoke_cert(
 1813     ca_name,
 1814     CN,
 1815     cacert_path=None,
 1816     ca_filename=None,
 1817     cert_path=None,
 1818     cert_filename=None,
 1819     crl_file=None,
 1820     digest="sha256",
 1821 ):
 1822     """
 1823     Revoke a certificate.
 1824 
 1825     .. versionadded:: 2015.8.0
 1826 
 1827     ca_name
 1828         Name of the CA.
 1829 
 1830     CN
 1831         Common name matching the certificate signing request.
 1832 
 1833     cacert_path
 1834         Absolute path to ca certificates root directory.
 1835 
 1836     ca_filename
 1837         Alternative filename for the CA.
 1838 
 1839     cert_path
 1840         Path to the cert file.
 1841 
 1842     cert_filename
 1843         Alternative filename for the certificate, useful when using special
 1844         characters in the CN.
 1845 
 1846     crl_file
 1847         Full path to the CRL file.
 1848 
 1849     digest
 1850         The message digest algorithm. Must be a string describing a digest
 1851         algorithm supported by OpenSSL (by EVP_get_digestbyname, specifically).
 1852         For example, "md5" or "sha1". Default: 'sha256'
 1853 
 1854     CLI Example:
 1855 
 1856     .. code-block:: bash
 1857 
 1858         salt '*' tls.revoke_cert ca_name='koji' \
 1859                 ca_filename='ca' \
 1860                 crl_file='/etc/openvpn/team1/crl.pem'
 1861 
 1862     """
 1863 
 1864     set_ca_path(cacert_path)
 1865     ca_dir = "{}/{}".format(cert_base_path(), ca_name)
 1866 
 1867     if ca_filename is None:
 1868         ca_filename = "{}_ca_cert".format(ca_name)
 1869 
 1870     if cert_path is None:
 1871         cert_path = "{}/{}/certs".format(_cert_base_path(), ca_name)
 1872 
 1873     if cert_filename is None:
 1874         cert_filename = "{}".format(CN)
 1875 
 1876     try:
 1877         with salt.utils.files.fopen(
 1878             "{}/{}/{}.crt".format(cert_base_path(), ca_name, ca_filename)
 1879         ) as fp_:
 1880             ca_cert = OpenSSL.crypto.load_certificate(
 1881                 OpenSSL.crypto.FILETYPE_PEM, fp_.read()
 1882             )
 1883         with salt.utils.files.fopen(
 1884             "{}/{}/{}.key".format(cert_base_path(), ca_name, ca_filename)
 1885         ) as fp_:
 1886             ca_key = OpenSSL.crypto.load_privatekey(
 1887                 OpenSSL.crypto.FILETYPE_PEM, fp_.read()
 1888             )
 1889     except OSError:
 1890         return 'There is no CA named "{}"'.format(ca_name)
 1891 
 1892     client_cert = _read_cert("{}/{}.crt".format(cert_path, cert_filename))
 1893     if client_cert is None:
 1894         return 'There is no client certificate named "{}"'.format(CN)
 1895 
 1896     index_file, expire_date, serial_number, subject = _get_basic_info(
 1897         ca_name, client_cert, ca_dir
 1898     )
 1899 
 1900     index_serial_subject = "{}\tunknown\t{}".format(serial_number, subject)
 1901     index_v_data = "V\t{}\t\t{}".format(expire_date, index_serial_subject)
 1902     index_r_data_pattern = re.compile(
 1903         r"R\t" + expire_date + r"\t\d{12}Z\t" + re.escape(index_serial_subject)
 1904     )
 1905     index_r_data = "R\t{}\t{}\t{}".format(
 1906         expire_date,
 1907         _four_digit_year_to_two_digit(datetime.utcnow()),
 1908         index_serial_subject,
 1909     )
 1910 
 1911     ret = {}
 1912     with salt.utils.files.fopen(index_file) as fp_:
 1913         for line in fp_:
 1914             line = salt.utils.stringutils.to_unicode(line)
 1915             if index_r_data_pattern.match(line):
 1916                 revoke_date = line.split("\t")[2]
 1917                 try:
 1918                     datetime.strptime(revoke_date, two_digit_year_fmt)
 1919                     return (
 1920                         '"{}/{}.crt" was already revoked, ' "serial number: {}"
 1921                     ).format(cert_path, cert_filename, serial_number)
 1922                 except ValueError:
 1923                     ret["retcode"] = 1
 1924                     ret["comment"] = (
 1925                         "Revocation date '{}' does not match"
 1926                         "format '{}'".format(revoke_date, two_digit_year_fmt)
 1927                     )
 1928                     return ret
 1929             elif index_serial_subject in line:
 1930                 __salt__["file.replace"](
 1931                     index_file, index_v_data, index_r_data, backup=False
 1932                 )
 1933                 break
 1934 
 1935     crl = OpenSSL.crypto.CRL()
 1936 
 1937     with salt.utils.files.fopen(index_file) as fp_:
 1938         for line in fp_:
 1939             line = salt.utils.stringutils.to_unicode(line)
 1940             if line.startswith("R"):
 1941                 fields = line.split("\t")
 1942                 revoked = OpenSSL.crypto.Revoked()
 1943                 revoked.set_serial(salt.utils.stringutils.to_bytes(fields[3]))
 1944                 revoke_date_2_digit = datetime.strptime(fields[2], two_digit_year_fmt)
 1945                 revoked.set_rev_date(
 1946                     salt.utils.stringutils.to_bytes(
 1947                         revoke_date_2_digit.strftime(four_digit_year_fmt)
 1948                     )
 1949                 )
 1950                 crl.add_revoked(revoked)
 1951 
 1952     crl_text = crl.export(
 1953         ca_cert, ca_key, digest=salt.utils.stringutils.to_bytes(digest)
 1954     )
 1955 
 1956     if crl_file is None:
 1957         crl_file = "{}/{}/crl.pem".format(_cert_base_path(), ca_name)
 1958 
 1959     if os.path.isdir(crl_file):
 1960         ret["retcode"] = 1
 1961         ret["comment"] = 'crl_file "{}" is an existing directory'.format(crl_file)
 1962         return ret
 1963 
 1964     with salt.utils.files.fopen(crl_file, "w") as fp_:
 1965         fp_.write(salt.utils.stringutils.to_str(crl_text))
 1966 
 1967     return ('Revoked Certificate: "{}/{}.crt", ' "serial number: {}").format(
 1968         cert_path, cert_filename, serial_number
 1969     )
 1970 
 1971 
 1972 if __name__ == "__main__":
 1973     # create_ca('koji', days=365, **cert_sample_meta)
 1974     create_csr(
 1975         "koji",
 1976         CN="test_system",
 1977         C="US",
 1978         ST="Utah",
 1979         L="Centerville",
 1980         O="SaltStack",
 1981         OU=None,
 1982         emailAddress="test_system@saltstack.org",
 1983     )
 1984     create_ca_signed_cert("koji", "test_system")
 1985     create_pkcs12("koji", "test_system", passphrase="test")