|
| 1 | +# -*- coding: utf-8 -*- |
| 2 | +''' |
| 3 | +Module to interact with keystores |
| 4 | +''' |
| 5 | + |
| 6 | +# Import Python libs |
| 7 | +from __future__ import absolute_import, unicode_literals, print_function |
| 8 | +import logging |
| 9 | +from datetime import datetime |
| 10 | +import os |
| 11 | + |
| 12 | +log = logging.getLogger(__name__) |
| 13 | + |
| 14 | +__virtualname__ = 'keystore' |
| 15 | + |
| 16 | +# Import third party libs |
| 17 | +from salt.exceptions import CommandExecutionError, SaltInvocationError |
| 18 | + |
| 19 | +try: |
| 20 | + import jks |
| 21 | + import OpenSSL |
| 22 | + has_depends = True |
| 23 | +except ImportError: |
| 24 | + has_depends = False |
| 25 | + |
| 26 | + |
| 27 | +def __virtual__(): |
| 28 | + ''' |
| 29 | + Check dependencies |
| 30 | + ''' |
| 31 | + if has_depends is False: |
| 32 | + msg = 'jks unavailable: {0} execution module cant be loaded '.format(__virtualname__) |
| 33 | + return False, msg |
| 34 | + return __virtualname__ |
| 35 | + |
| 36 | + |
| 37 | +def _parse_cert(alias, public_cert, return_cert=False): |
| 38 | + ASN1 = OpenSSL.crypto.FILETYPE_ASN1 |
| 39 | + PEM = OpenSSL.crypto.FILETYPE_PEM |
| 40 | + cert_data = {} |
| 41 | + sha1 = public_cert.digest(b'sha1') |
| 42 | + |
| 43 | + cert_pem = OpenSSL.crypto.dump_certificate(PEM, public_cert) |
| 44 | + raw_until = public_cert.get_notAfter() |
| 45 | + date_until = datetime.strptime(raw_until, '%Y%m%d%H%M%SZ') |
| 46 | + string_until = date_until.strftime("%B %d %Y") |
| 47 | + |
| 48 | + raw_start = public_cert.get_notBefore() |
| 49 | + date_start = datetime.strptime(raw_start, '%Y%m%d%H%M%SZ') |
| 50 | + string_start = date_start.strftime("%B %d %Y") |
| 51 | + |
| 52 | + if return_cert: |
| 53 | + cert_data['pem'] = cert_pem |
| 54 | + cert_data['alias'] = alias |
| 55 | + cert_data['sha1'] = sha1 |
| 56 | + cert_data['valid_until'] = string_until |
| 57 | + cert_data['valid_start'] = string_start |
| 58 | + cert_data['expired'] = date_until < datetime.now() |
| 59 | + |
| 60 | + return cert_data |
| 61 | + |
| 62 | + |
| 63 | +def list(keystore, passphrase, alias=None, return_cert=False): |
| 64 | + ''' |
| 65 | + Lists certificates in a keytool managed keystore. |
| 66 | +
|
| 67 | +
|
| 68 | + :param keystore: The path to the keystore file to query |
| 69 | + :param passphrase: The passphrase to use to decode the keystore |
| 70 | + :param alias: (Optional) If found, displays details on only this key |
| 71 | + :param return_certs: (Optional) Also return certificate PEM. |
| 72 | +
|
| 73 | + .. warning:: |
| 74 | +
|
| 75 | + There are security implications for using return_cert to return decrypted certificates. |
| 76 | +
|
| 77 | + CLI Example: |
| 78 | +
|
| 79 | + .. code-block:: bash |
| 80 | +
|
| 81 | + salt '*' keystore.list /usr/lib/jvm/java-8/jre/lib/security/cacerts changeit |
| 82 | + salt '*' keystore.list /usr/lib/jvm/java-8/jre/lib/security/cacerts changeit debian:verisign_-_g5.pem |
| 83 | +
|
| 84 | + ''' |
| 85 | + ASN1 = OpenSSL.crypto.FILETYPE_ASN1 |
| 86 | + PEM = OpenSSL.crypto.FILETYPE_PEM |
| 87 | + decoded_certs = [] |
| 88 | + entries = [] |
| 89 | + |
| 90 | + keystore = jks.KeyStore.load(keystore, passphrase) |
| 91 | + |
| 92 | + if alias: |
| 93 | + # If alias is given, look it up and build expected data structure |
| 94 | + entry_value = keystore.entries.get(alias) |
| 95 | + if entry_value: |
| 96 | + entries = [(alias, entry_value)] |
| 97 | + else: |
| 98 | + entries = keystore.entries.items() |
| 99 | + |
| 100 | + if entries: |
| 101 | + for entry_alias, cert_enc in entries: |
| 102 | + entry_data = {} |
| 103 | + if isinstance(cert_enc, jks.PrivateKeyEntry): |
| 104 | + cert_result = cert_enc.cert_chain[0][1] |
| 105 | + entry_data['type'] = 'PrivateKeyEntry' |
| 106 | + elif isinstance(cert_enc, jks.TrustedCertEntry): |
| 107 | + cert_result = cert_enc.cert |
| 108 | + entry_data['type'] = 'TrustedCertEntry' |
| 109 | + else: |
| 110 | + raise CommandExecutionError('Unsupported EntryType detected in keystore') |
| 111 | + |
| 112 | + # Detect if ASN1 binary, otherwise assume PEM |
| 113 | + if '\x30' in cert_result[0]: |
| 114 | + public_cert = OpenSSL.crypto.load_certificate(ASN1, cert_result) |
| 115 | + else: |
| 116 | + public_cert = OpenSSL.crypto.load_certificate(PEM, cert_result) |
| 117 | + |
| 118 | + entry_data.update(_parse_cert(entry_alias, public_cert, return_cert)) |
| 119 | + decoded_certs.append(entry_data) |
| 120 | + |
| 121 | + return decoded_certs |
| 122 | + |
| 123 | + |
| 124 | +def add(name, keystore, passphrase, certificate, private_key=None): |
| 125 | + ''' |
| 126 | + Adds certificates to an existing keystore or creates a new one if necesssary. |
| 127 | +
|
| 128 | + :param name: alias for the certificate |
| 129 | + :param keystore: The path to the keystore file to query |
| 130 | + :param passphrase: The passphrase to use to decode the keystore |
| 131 | + :param certificate: The PEM public certificate to add to keystore. Can be a string for file. |
| 132 | + :param private_key: (Optional for TrustedCert) The PEM private key to add to the keystore |
| 133 | +
|
| 134 | + CLI Example: |
| 135 | +
|
| 136 | + .. code-block:: bash |
| 137 | +
|
| 138 | + salt '*' keystore.add aliasname /tmp/test.store changeit /tmp/testcert.crt |
| 139 | + salt '*' keystore.add aliasname /tmp/test.store changeit certificate="-----BEGIN CERTIFICATE-----SIb...BM=-----END CERTIFICATE-----" |
| 140 | + salt '*' keystore.add keyname /tmp/test.store changeit /tmp/512.cert private_key=/tmp/512.key |
| 141 | +
|
| 142 | + ''' |
| 143 | + ASN1 = OpenSSL.crypto.FILETYPE_ASN1 |
| 144 | + PEM = OpenSSL.crypto.FILETYPE_PEM |
| 145 | + certs_list = [] |
| 146 | + if os.path.isfile(keystore): |
| 147 | + keystore_object = jks.KeyStore.load(keystore, passphrase) |
| 148 | + for alias, loaded_cert in keystore_object.entries.items(): |
| 149 | + certs_list.append(loaded_cert) |
| 150 | + |
| 151 | + try: |
| 152 | + cert_string = __salt__['x509.get_pem_entry'](certificate) |
| 153 | + except SaltInvocationError: |
| 154 | + raise SaltInvocationError('Invalid certificate file or string: {0}'.format(certificate)) |
| 155 | + |
| 156 | + if private_key: |
| 157 | + # Accept PEM input format, but convert to DES for loading into new keystore |
| 158 | + key_string = __salt__['x509.get_pem_entry'](private_key) |
| 159 | + loaded_cert = OpenSSL.crypto.load_certificate(PEM, cert_string) |
| 160 | + loaded_key = OpenSSL.crypto.load_privatekey(PEM, key_string) |
| 161 | + dumped_cert = OpenSSL.crypto.dump_certificate(ASN1, loaded_cert) |
| 162 | + dumped_key = OpenSSL.crypto.dump_privatekey(ASN1, loaded_key) |
| 163 | + |
| 164 | + new_entry = jks.PrivateKeyEntry.new(name, [dumped_cert], dumped_key, 'rsa_raw') |
| 165 | + else: |
| 166 | + new_entry = jks.TrustedCertEntry.new(name, cert_string) |
| 167 | + |
| 168 | + certs_list.append(new_entry) |
| 169 | + |
| 170 | + keystore_object = jks.KeyStore.new('jks', certs_list) |
| 171 | + keystore_object.save(keystore, passphrase) |
| 172 | + return True |
| 173 | + |
| 174 | + |
| 175 | +def remove(name, keystore, passphrase): |
| 176 | + ''' |
| 177 | + Removes a certificate from an existing keystore. |
| 178 | + Returns True if remove was successful, otherwise False |
| 179 | +
|
| 180 | + :param name: alias for the certificate |
| 181 | + :param keystore: The path to the keystore file to query |
| 182 | + :param passphrase: The passphrase to use to decode the keystore |
| 183 | +
|
| 184 | + CLI Example: |
| 185 | +
|
| 186 | + .. code-block:: bash |
| 187 | +
|
| 188 | + salt '*' keystore.remove aliasname /tmp/test.store changeit |
| 189 | + ''' |
| 190 | + certs_list = [] |
| 191 | + keystore_object = jks.KeyStore.load(keystore, passphrase) |
| 192 | + for alias, loaded_cert in keystore_object.entries.items(): |
| 193 | + if name not in alias: |
| 194 | + certs_list.append(loaded_cert) |
| 195 | + |
| 196 | + if len(keystore_object.entries) != len(certs_list): |
| 197 | + # Entry has been removed, save keystore updates |
| 198 | + keystore_object = jks.KeyStore.new('jks', certs_list) |
| 199 | + keystore_object.save(keystore, passphrase) |
| 200 | + return True |
| 201 | + else: |
| 202 | + # No alias found, notify user |
| 203 | + return False |
0 commit comments