diff --git a/changelog/62978.added b/changelog/62978.added new file mode 100644 index 000000000000..3262ce53a0b5 --- /dev/null +++ b/changelog/62978.added @@ -0,0 +1 @@ +Added output and bare functionality to export_key gpg module function diff --git a/salt/modules/gpg.py b/salt/modules/gpg.py index 46799372eb3a..44feb4822f2a 100644 --- a/salt/modules/gpg.py +++ b/salt/modules/gpg.py @@ -783,16 +783,21 @@ def import_key(text=None, filename=None, user=None, gnupghome=None): def export_key( - keyids=None, secret=False, user=None, gnupghome=None, use_passphrase=False + keyids=None, + secret=False, + user=None, + gnupghome=None, + use_passphrase=False, + output=None, + bare=False, ): """ Export a key from the GPG keychain keyids The key ID(s) of the key(s) to be exported. Can be specified as a comma - separated string or a list. Anything which GnuPG itself accepts to - identify a key - for example, the key ID or the fingerprint could be - used. + separated string or a list. Anything which GnuPG itself accepts to identify a key + for example, the key ID, fingerprint, user ID or email address could be used. secret Export the secret key identified by the ``keyids`` information passed. @@ -806,11 +811,22 @@ def export_key( Specify the location where GPG keyring and related files are stored. use_passphrase - Whether to use a passphrase with the signing key. Passphrase is received - from Pillar. + Whether to use a passphrase to export the secret key. + Passphrase is received from Pillar. .. versionadded:: 3003 + output + The filename where the exported key data will be written to, default is standard out. + + .. versionadded:: 3006.0 + + bare + If ``True``, return the (armored) exported key block as a string without the + standard comment/res dict. + + .. versionadded:: 3006.0 + CLI Example: .. code-block:: bash @@ -822,18 +838,40 @@ def export_key( salt '*' gpg.export_key keyids="['3FAD9F1E','3FBD8F1E']" user=username """ + ret = {"res": True} gpg = _create_gpg(user, gnupghome) if isinstance(keyids, str): keyids = keyids.split(",") - if use_passphrase: + if secret and use_passphrase: gpg_passphrase = __salt__["pillar.get"]("gpg_passphrase") if not gpg_passphrase: raise SaltInvocationError("gpg_passphrase not available in pillar.") - ret = gpg.export_keys(keyids, secret, passphrase=gpg_passphrase) + result = gpg.export_keys(keyids, secret, passphrase=gpg_passphrase) else: - ret = gpg.export_keys(keyids, secret, expect_passphrase=False) + result = gpg.export_keys(keyids, secret, expect_passphrase=False) + + if result and output: + with salt.utils.files.flopen(output, "w") as fout: + fout.write(salt.utils.stringutils.to_str(result)) + + if result: + if not bare: + if output: + ret["comment"] = "Exported key data has been written to {}".format( + output + ) + else: + ret["comment"] = result + else: + ret = result + else: + if not bare: + ret["res"] = False + else: + ret = False + return ret diff --git a/tests/pytests/unit/modules/test_gpg.py b/tests/pytests/unit/modules/test_gpg.py index d87df2274fa5..7ac084e612bc 100644 --- a/tests/pytests/unit/modules/test_gpg.py +++ b/tests/pytests/unit/modules/test_gpg.py @@ -5,6 +5,7 @@ import datetime import logging +import pathlib import shutil import subprocess import time @@ -14,7 +15,6 @@ import pytest import salt.modules.gpg as gpg -from salt.exceptions import SaltInvocationError from tests.support.mock import MagicMock, patch pytest.importorskip("gnupg") @@ -552,9 +552,9 @@ def test_delete_key_with_passphrase_with_gpg_passphrase_in_pillar(gpghome): ) -def test_export_key_without_passphrase(gpghome): +def test_export_public_key(gpghome): """ - Test gpg.export_key without passphrase + Test gpg.export_key with single public key """ _user_mock = { @@ -578,7 +578,7 @@ def test_export_key_without_passphrase(gpghome): "salt.modules.gpg.gnupg.GPG.export_keys", MagicMock(return_value=GPG_TEST_PUB_KEY), ) as gnupg_export_keys: - ret = gpg.export_key("xxxxxxxxxxxxxxxx") + ret = gpg.export_key("xxxxxxxxxxxxxxxx", bare=True) assert ret == GPG_TEST_PUB_KEY gnupg_export_keys.assert_called_with( ["xxxxxxxxxxxxxxxx"], @@ -587,9 +587,46 @@ def test_export_key_without_passphrase(gpghome): ) -def test_export_multiple_keys_without_passphrase(gpghome): +def test_export_public_key_to_file(gpghome): """ - Test gpg.export_key with multiple keys and without passphrase + Test gpg.export_key output public key to file + """ + + _user_mock = { + "shell": "/bin/bash", + "workphone": "", + "uid": 0, + "passwd": "x", + "roomnumber": "", + "gid": 0, + "groups": ["root"], + "home": str(gpghome.path), + "fullname": "root", + "homephone": "", + "name": "root", + } + + exported_keyfile = gpghome.path / "exported_pub_key" + mock_opt = MagicMock(return_value="root") + pillar_mock = MagicMock(return_value=GPG_TEST_KEY_PASSPHRASE) + + with patch.dict(gpg.__salt__, {"user.info": MagicMock(return_value=_user_mock)}): + with patch.dict(gpg.__salt__, {"config.option": mock_opt}): + with patch( + "salt.modules.gpg.gnupg.GPG.export_keys", + MagicMock(return_value=GPG_TEST_PUB_KEY), + ) as gnupg_export_keys: + ret = gpg.export_key( + keyids="xxxxxxxxxxxxxxxx", output=exported_keyfile, bare=True + ) + assert ret == GPG_TEST_PUB_KEY + keyfile_contents = pathlib.Path(exported_keyfile).read_text() + assert keyfile_contents == GPG_TEST_PUB_KEY + + +def test_export_multiple_public_keys(gpghome): + """ + Test gpg.export_key with multiple public keys """ _user_mock = { @@ -614,7 +651,8 @@ def test_export_multiple_keys_without_passphrase(gpghome): MagicMock(return_value=GPG_TEST_PUB_KEY), ) as gnupg_export_keys: ret = gpg.export_key( - "xxxxxxxxxxxxxxxx,yyyyyyyyyyyyyyyy,zzzzzzzzzzzzzzzz" + "xxxxxxxxxxxxxxxx,yyyyyyyyyyyyyyyy,zzzzzzzzzzzzzzzz", + bare=True, ) assert ret == GPG_TEST_PUB_KEY gnupg_export_keys.assert_called_with( @@ -624,9 +662,9 @@ def test_export_multiple_keys_without_passphrase(gpghome): ) -def test_export_key_with_passphrase_without_gpg_passphrase_in_pillar(gpghome): +def test_export_secret_key_with_gpg_passphrase_in_pillar(gpghome): """ - Test gpg.export_key with passphrase but without gpg_passphrase pillar + Test gpg.export_key with secret key and gpg_passphrase in pillar """ _user_mock = { @@ -644,23 +682,29 @@ def test_export_key_with_passphrase_without_gpg_passphrase_in_pillar(gpghome): } mock_opt = MagicMock(return_value="root") - pillar_mock = MagicMock(return_value=None) + pillar_mock = MagicMock(return_value=GPG_TEST_KEY_PASSPHRASE) with patch.dict(gpg.__salt__, {"user.info": MagicMock(return_value=_user_mock)}): with patch.dict(gpg.__salt__, {"config.option": mock_opt}), patch.dict( gpg.__salt__, {"pillar.get": pillar_mock} ): with patch( "salt.modules.gpg.gnupg.GPG.export_keys", - MagicMock(return_value=GPG_TEST_PUB_KEY), + MagicMock(return_value=GPG_TEST_PRIV_KEY), ) as gnupg_export_keys: - with pytest.raises(SaltInvocationError): - assert gpg.export_key("xxxxxxxxxxxxxxxx", use_passphrase=True) - gnupg_export_keys.assert_not_called() + ret = gpg.export_key( + "xxxxxxxxxxxxxxxx", secret=True, use_passphrase=True, bare=True + ) + assert ret == GPG_TEST_PRIV_KEY + gnupg_export_keys.assert_called_with( + ["xxxxxxxxxxxxxxxx"], + True, + passphrase=GPG_TEST_KEY_PASSPHRASE, + ) -def test_export_key_with_passphrase_with_gpg_passphrase_in_pillar(gpghome): +def test_export_secret_key_to_file_with_gpg_passphrase_in_pillar(gpghome): """ - Test gpg.export_key with passphrase and gpg_passphrase pillar + Test gpg.export_key output secret key to file with gpg_passphrase in pillar """ _user_mock = { @@ -677,6 +721,7 @@ def test_export_key_with_passphrase_with_gpg_passphrase_in_pillar(gpghome): "name": "root", } + exported_keyfile = gpghome.path / "exported_priv_key" mock_opt = MagicMock(return_value="root") pillar_mock = MagicMock(return_value=GPG_TEST_KEY_PASSPHRASE) with patch.dict(gpg.__salt__, {"user.info": MagicMock(return_value=_user_mock)}): @@ -685,15 +730,23 @@ def test_export_key_with_passphrase_with_gpg_passphrase_in_pillar(gpghome): ): with patch( "salt.modules.gpg.gnupg.GPG.export_keys", - MagicMock(return_value=GPG_TEST_PUB_KEY), + MagicMock(return_value=GPG_TEST_PRIV_KEY), ) as gnupg_export_keys: - ret = gpg.export_key("xxxxxxxxxxxxxxxx", use_passphrase=True) - assert ret == GPG_TEST_PUB_KEY + ret = gpg.export_key( + keyids="xxxxxxxxxxxxxxxx", + secret=True, + output=exported_keyfile, + use_passphrase=True, + bare=True, + ) + assert ret == GPG_TEST_PRIV_KEY gnupg_export_keys.assert_called_with( ["xxxxxxxxxxxxxxxx"], - False, + True, passphrase=GPG_TEST_KEY_PASSPHRASE, ) + keyfile_contents = pathlib.Path(exported_keyfile).read_text() + assert keyfile_contents == GPG_TEST_PRIV_KEY def test_create_key_without_passphrase(gpghome): @@ -857,6 +910,10 @@ def test_gpg_import_priv_key(gpghome): def test_gpg_sign(gpghome): + """ + Test gpg.sign + """ + config_user = MagicMock(return_value="salt") user_info = MagicMock( return_value={"name": "salt", "home": str(gpghome.path), "uid": 1000} @@ -878,6 +935,10 @@ def test_gpg_sign(gpghome): def test_gpg_encrypt_message(gpghome): + """ + Test gpg.encrypt + """ + config_user = MagicMock(return_value="salt") user_info = MagicMock( @@ -899,6 +960,10 @@ def test_gpg_encrypt_message(gpghome): def test_gpg_encrypt_and_sign_message_with_gpg_passphrase_in_pillar(gpghome): + """ + Test gpg.encrypt sign message with passphrase and gpg_passphrase in pillar + """ + config_user = MagicMock(return_value="salt") user_info = MagicMock( @@ -924,8 +989,9 @@ def test_gpg_encrypt_and_sign_message_with_gpg_passphrase_in_pillar(gpghome): def test_gpg_decrypt_message_with_gpg_passphrase_in_pillar(gpghome): """ - Test gpg.decrypt with passphrase and gpg_passphrase pillar + Test gpg.decrypt with passphrase and gpg_passphrase in pillar """ + gpg_encrypted_message = """-----BEGIN PGP MESSAGE----- hQGMA7z9rKs9ZvTOAQwAnMbwchCm1VXOD+Ml0rnNrhDhsRm+6O96FOq5lWY0ntkj vnXeFOgUf0wzK4hkQT/Yo4/ZpDkV3iwwSIjesqNDS1U/KWfbe2pFeph6w9fHFnXf