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

gpg.export_key add output and bare functionality #62979

Merged
merged 12 commits into from
Nov 9, 2022
1 change: 1 addition & 0 deletions changelog/62978.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added output and bare functionality to export_key gpg module function
52 changes: 43 additions & 9 deletions salt/modules/gpg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Ch3LL marked this conversation as resolved.
Show resolved Hide resolved
output=None,
use_passphrase=False,
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.
Expand All @@ -805,12 +810,19 @@ def export_key(
gnupghome
Specify the location where GPG keyring and related files are stored.

output
Ch3LL marked this conversation as resolved.
Show resolved Hide resolved
The filename where the exported key data will be written to, default is standard out.

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

bare
Ch3LL marked this conversation as resolved.
Show resolved Hide resolved
If ``True``, return the (armored) exported key block as a string without the
standard comment/res dict.

CLI Example:

.. code-block:: bash
Expand All @@ -822,18 +834,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:
Ch3LL marked this conversation as resolved.
Show resolved Hide resolved
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


Expand Down
108 changes: 87 additions & 21 deletions tests/pytests/unit/modules/test_gpg.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import datetime
import logging
import pathlib
import shutil
import subprocess
import time
Expand All @@ -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")
Expand Down Expand Up @@ -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 = {
Expand All @@ -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"],
Expand All @@ -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 = {
Expand All @@ -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(
Expand All @@ -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 = {
Expand All @@ -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 = {
Expand All @@ -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)}):
Expand All @@ -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):
Expand Down Expand Up @@ -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}
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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
Expand Down