From 507a73df03b50cf0392d28fd72614449f561a3dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lemercier?= Date: Mon, 13 Apr 2026 17:06:05 +0200 Subject: [PATCH 1/2] Fix gen_signature signing raw pub key instead of clean_key'd content gen_signature() was signing the raw pub key file content (which includes a trailing newline), but get_pub_str() sends clean_key(pub) to minions. This mismatch caused verify_pubkey_sig() to always fail when master_use_pubkey_signature was enabled. Apply clean_key() to the file content before signing so the signature matches what minions receive and verify against. Fixes: #68930 --- salt/crypt.py | 2 +- tests/pytests/unit/test_crypt.py | 42 +++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/salt/crypt.py b/salt/crypt.py index a53786e4f03d..e7adb806cbe4 100644 --- a/salt/crypt.py +++ b/salt/crypt.py @@ -395,7 +395,7 @@ def gen_signature(priv_path, pub_path, sign_path, passphrase=None): """ with salt.utils.files.fopen(pub_path) as fp_: - mpub_64 = fp_.read() + mpub_64 = clean_key(fp_.read()) mpub_sig = sign_message(priv_path, mpub_64, passphrase) mpub_sig_64 = binascii.b2a_base64(mpub_sig) diff --git a/tests/pytests/unit/test_crypt.py b/tests/pytests/unit/test_crypt.py index 96b37dbe9cc7..9c7771d410ff 100644 --- a/tests/pytests/unit/test_crypt.py +++ b/tests/pytests/unit/test_crypt.py @@ -5,7 +5,7 @@ import salt.crypt as crypt import salt.exceptions -from tests.support.mock import patch +from tests.support.mock import mock_open, patch @pytest.fixture @@ -243,3 +243,43 @@ def test_async_auth_cache_token(minion_root, io_loop): auth.gen_token("salt") auth.gen_token("salt") moc.assert_called_once() + + +@pytest.mark.parametrize("linesep", ["\r\n", "\r", "\n"]) +def test_gen_signature_signs_clean_key(key_data, linesep): + """ + Regression test for https://github.com/saltstack/salt/issues/68930 + + gen_signature() must apply clean_key() before signing so the signed + content matches what get_pub_str() sends to minions. + """ + raw_pub_on_disk = linesep.join(key_data) + expected = crypt.clean_key(raw_pub_on_disk) + + with patch("salt.utils.files.fopen", mock_open(read_data=raw_pub_on_disk)), \ + patch("os.path.isfile", return_value=False), \ + patch("salt.crypt.sign_message", return_value=b"fakesig") as mock_sign: + crypt.gen_signature("priv_path", "pub_path", "sig_path") + + _, signed_content, _ = mock_sign.call_args[0] + assert signed_content == expected + + +@pytest.mark.parametrize("linesep", ["\r\n", "\r", "\n"]) +def test_gen_signature_signs_clean_key_trailing_newline(key_data, linesep): + """ + Same as above but with a trailing newline, which is the common case + because the cryptography library writes PEM files with one. + """ + raw_pub_on_disk = linesep.join(key_data) + linesep + expected = crypt.clean_key(raw_pub_on_disk) + + assert raw_pub_on_disk != expected + + with patch("salt.utils.files.fopen", mock_open(read_data=raw_pub_on_disk)), \ + patch("os.path.isfile", return_value=False), \ + patch("salt.crypt.sign_message", return_value=b"fakesig") as mock_sign: + crypt.gen_signature("priv_path", "pub_path", "sig_path") + + _, signed_content, _ = mock_sign.call_args[0] + assert signed_content == expected From 81d069505c67e2ca4785b40b311b6f3c662963d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Lemercier?= Date: Mon, 13 Apr 2026 17:06:22 +0200 Subject: [PATCH 2/2] Add changelog entry for #68930 --- changelog/68930.fixed | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/68930.fixed diff --git a/changelog/68930.fixed b/changelog/68930.fixed new file mode 100644 index 000000000000..1a46d7029524 --- /dev/null +++ b/changelog/68930.fixed @@ -0,0 +1 @@ +Fixed gen_signature() signing raw pub key content instead of clean_key'd content, causing master_use_pubkey_signature verification to always fail.