Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add keystore execution and state modules #52126

Merged
merged 14 commits into from
Mar 18, 2019
1 change: 1 addition & 0 deletions doc/ref/modules/all/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ execution modules
keyboard
keystone
keystoneng
keystore
kmod
kubernetes
launchctl_service
Expand Down
6 changes: 6 additions & 0 deletions doc/ref/modules/all/salt.modules.keystore.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
=====================
salt.modules.keystore
=====================

.. automodule:: salt.modules.keystore
:members:
1 change: 1 addition & 0 deletions doc/ref/states/all/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ state modules
keystone_role_grant
keystone_service
keystone_user
keystore
kmod
kubernetes
layman
Expand Down
6 changes: 6 additions & 0 deletions doc/ref/states/all/salt.states.keystore.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
====================
salt.states.keystore
====================

.. automodule:: salt.states.keystore
:members:
79 changes: 65 additions & 14 deletions doc/topics/releases/neon.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@ to see the data loaded from a jinja map, or imported using ``import_yaml`` or
Saltcheck Updates
=================

Available since 2018.3, the saltcheck module has been enhanced to:
* Support saltenv environments
* Associate tests with states by naming convention
* Adds empty and notempty assertions
* Adds skip keyword
* Adds print_result keyword
* Adds assertion_section keyword
* Use saltcheck.state_apply to run state.apply for test setup or teardown
* Changes output to display test time
Available since 2018.3, the :py:func:`saltcheck module <salt.modules.saltcheck>`
has been enhanced to:
* Support saltenv environments
* Associate tests with states by naming convention
* Adds empty and notempty assertions
* Adds skip keyword
* Adds print_result keyword
* Adds assertion_section keyword
* Use saltcheck.state_apply to run state.apply for test setup or teardown
* Changes output to display test time

Saltcheck provides unittest like functionality requiring only the knowledge of
salt module execution and yaml. Saltcheck uses salt modules to return data, then
Expand Down Expand Up @@ -173,11 +174,62 @@ New output:
Skipped:
0

XML Module
==========

A new state and execution module for editing XML files is now included. Currently it allows for
editing values from an xpath query, or editing XML IDs.
Keystore State and Module
=========================

A new :py:func:`state <salt.states.keystore>` and
:py:func:`execution module <salt.modules.keystore>` for manaing Java
Keystore files is now included. It allows for adding/removing/listing
as well as managing keystore files.

.. code-block:: bash

# salt-call keystore.list /path/to/keystore.jks changeit
local:
|_
----------
alias:
hostname1
expired:
True
sha1:
CB:5E:DE:50:57:99:51:87:8E:2E:67:13:C5:3B:E9:38:EB:23:7E:40
type:
TrustedCertEntry
valid_start:
August 22 2012
valid_until:
August 21 2017

.. code-block:: yaml

define_keystore:
keystore.managed:
- name: /tmp/statestore.jks
- passphrase: changeit
- force_remove: True
- entries:
- alias: hostname1
certificate: /tmp/testcert.crt
- alias: remotehost
certificate: /tmp/512.cert
private_key: /tmp/512.key
- alias: stringhost
certificate: |
-----BEGIN CERTIFICATE-----
MIICEjCCAX
Hn+GmxZA
-----END CERTIFICATE-----


XML State and Module
====================

A new :py:func:`state <salt.states.xml>` and
:py:func:`execution module <salt.modules.xml>` for editing XML files is
now included. Currently it allows for editing values from an xpath query, or
editing XML IDs.

.. code-block:: bash

Expand Down Expand Up @@ -210,7 +262,6 @@ editing values from an xpath query, or editing XML IDs.
- value: William Shatner



State Changes
=============

Expand Down
203 changes: 203 additions & 0 deletions salt/modules/keystore.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
# -*- coding: utf-8 -*-
'''
Module to interact with keystores
'''

# Import Python libs
from __future__ import absolute_import, unicode_literals, print_function
import logging
from datetime import datetime
import os

log = logging.getLogger(__name__)

__virtualname__ = 'keystore'

# Import third party libs
from salt.exceptions import CommandExecutionError, SaltInvocationError

try:
import jks
import OpenSSL
has_depends = True
except ImportError:
has_depends = False


def __virtual__():
'''
Check dependencies
'''
if has_depends is False:
msg = 'jks unavailable: {0} execution module cant be loaded '.format(__virtualname__)
return False, msg
return __virtualname__


def _parse_cert(alias, public_cert, return_cert=False):
ASN1 = OpenSSL.crypto.FILETYPE_ASN1
PEM = OpenSSL.crypto.FILETYPE_PEM
cert_data = {}
sha1 = public_cert.digest(b'sha1')

cert_pem = OpenSSL.crypto.dump_certificate(PEM, public_cert)
raw_until = public_cert.get_notAfter()
date_until = datetime.strptime(raw_until, '%Y%m%d%H%M%SZ')
string_until = date_until.strftime("%B %d %Y")

raw_start = public_cert.get_notBefore()
date_start = datetime.strptime(raw_start, '%Y%m%d%H%M%SZ')
string_start = date_start.strftime("%B %d %Y")

if return_cert:
cert_data['pem'] = cert_pem
cert_data['alias'] = alias
cert_data['sha1'] = sha1
cert_data['valid_until'] = string_until
cert_data['valid_start'] = string_start
cert_data['expired'] = date_until < datetime.now()

return cert_data


def list(keystore, passphrase, alias=None, return_cert=False):
'''
Lists certificates in a keytool managed keystore.


:param keystore: The path to the keystore file to query
:param passphrase: The passphrase to use to decode the keystore
:param alias: (Optional) If found, displays details on only this key
:param return_certs: (Optional) Also return certificate PEM.

.. warning::

There are security implications for using return_cert to return decrypted certificates.

CLI Example:

.. code-block:: bash

salt '*' keystore.list /usr/lib/jvm/java-8/jre/lib/security/cacerts changeit
salt '*' keystore.list /usr/lib/jvm/java-8/jre/lib/security/cacerts changeit debian:verisign_-_g5.pem

'''
ASN1 = OpenSSL.crypto.FILETYPE_ASN1
PEM = OpenSSL.crypto.FILETYPE_PEM
decoded_certs = []
entries = []

keystore = jks.KeyStore.load(keystore, passphrase)

if alias:
# If alias is given, look it up and build expected data structure
entry_value = keystore.entries.get(alias)
if entry_value:
entries = [(alias, entry_value)]
else:
entries = keystore.entries.items()

if entries:
for entry_alias, cert_enc in entries:
entry_data = {}
if isinstance(cert_enc, jks.PrivateKeyEntry):
cert_result = cert_enc.cert_chain[0][1]
entry_data['type'] = 'PrivateKeyEntry'
elif isinstance(cert_enc, jks.TrustedCertEntry):
cert_result = cert_enc.cert
entry_data['type'] = 'TrustedCertEntry'
else:
raise CommandExecutionError('Unsupported EntryType detected in keystore')

# Detect if ASN1 binary, otherwise assume PEM
if '\x30' in cert_result[0]:
public_cert = OpenSSL.crypto.load_certificate(ASN1, cert_result)
else:
public_cert = OpenSSL.crypto.load_certificate(PEM, cert_result)

entry_data.update(_parse_cert(entry_alias, public_cert, return_cert))
decoded_certs.append(entry_data)

return decoded_certs


def add(name, keystore, passphrase, certificate, private_key=None):
'''
Adds certificates to an existing keystore or creates a new one if necesssary.

:param name: alias for the certificate
:param keystore: The path to the keystore file to query
:param passphrase: The passphrase to use to decode the keystore
:param certificate: The PEM public certificate to add to keystore. Can be a string for file.
:param private_key: (Optional for TrustedCert) The PEM private key to add to the keystore

CLI Example:

.. code-block:: bash

salt '*' keystore.add aliasname /tmp/test.store changeit /tmp/testcert.crt
salt '*' keystore.add aliasname /tmp/test.store changeit certificate="-----BEGIN CERTIFICATE-----SIb...BM=-----END CERTIFICATE-----"
salt '*' keystore.add keyname /tmp/test.store changeit /tmp/512.cert private_key=/tmp/512.key

'''
ASN1 = OpenSSL.crypto.FILETYPE_ASN1
PEM = OpenSSL.crypto.FILETYPE_PEM
certs_list = []
if os.path.isfile(keystore):
keystore_object = jks.KeyStore.load(keystore, passphrase)
for alias, loaded_cert in keystore_object.entries.items():
certs_list.append(loaded_cert)

try:
cert_string = __salt__['x509.get_pem_entry'](certificate)
except SaltInvocationError:
raise SaltInvocationError('Invalid certificate file or string: {0}'.format(certificate))

if private_key:
# Accept PEM input format, but convert to DES for loading into new keystore
key_string = __salt__['x509.get_pem_entry'](private_key)
loaded_cert = OpenSSL.crypto.load_certificate(PEM, cert_string)
loaded_key = OpenSSL.crypto.load_privatekey(PEM, key_string)
dumped_cert = OpenSSL.crypto.dump_certificate(ASN1, loaded_cert)
dumped_key = OpenSSL.crypto.dump_privatekey(ASN1, loaded_key)

new_entry = jks.PrivateKeyEntry.new(name, [dumped_cert], dumped_key, 'rsa_raw')
else:
new_entry = jks.TrustedCertEntry.new(name, cert_string)

certs_list.append(new_entry)

keystore_object = jks.KeyStore.new('jks', certs_list)
keystore_object.save(keystore, passphrase)
return True


def remove(name, keystore, passphrase):
'''
Removes a certificate from an existing keystore.
Returns True if remove was successful, otherwise False

:param name: alias for the certificate
:param keystore: The path to the keystore file to query
:param passphrase: The passphrase to use to decode the keystore

CLI Example:

.. code-block:: bash

salt '*' keystore.remove aliasname /tmp/test.store changeit
'''
certs_list = []
keystore_object = jks.KeyStore.load(keystore, passphrase)
for alias, loaded_cert in keystore_object.entries.items():
if name not in alias:
certs_list.append(loaded_cert)

if len(keystore_object.entries) != len(certs_list):
# Entry has been removed, save keystore updates
keystore_object = jks.KeyStore.new('jks', certs_list)
keystore_object.save(keystore, passphrase)
return True
else:
# No alias found, notify user
return False
3 changes: 2 additions & 1 deletion salt/modules/saltcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,8 @@
* assertGreaterEqual
* assertLess
* assertLessEqual
* assertEmptyassertNotEmpty
* assertEmpty
* assertNotEmpty

.. warning::

Expand Down
Loading