What happened?
Description
When master_sign_pubkey: True and master_use_pubkey_signature: True are enabled, minions always fail to verify the pre-computed master public key signature with:
Received signed public-key from master <IP> but signature verification failed!
Dynamic signing (without master_use_pubkey_signature) works correctly.
Root Cause
gen_signature() in salt/crypt.py signs the raw file content of master.pub, which includes a trailing newline (\n) added by the cryptography library's PEM encoding (per RFC 7468):
def gen_signature(priv_path, pub_path, sign_path, passphrase=None):
with salt.utils.files.fopen(pub_path) as fp_:
mpub_64 = fp_.read() # <-- raw file: 451 bytes, ends with \n
mpub_sig = sign_message(priv_path, mpub_64, passphrase)
However, when the master sends its public key to minions during auth, it uses get_pub_str() which applies clean_key():
def get_pub_str(self, name="master"):
with salt.utils.files.fopen(path) as rfh:
return clean_key(rfh.read()) # <-- stripped: 450 bytes, no trailing \n
And clean_key() strips the trailing newline:
def clean_key(key):
return "\n".join(key.strip().splitlines())
In salt/channel/server.py, the auth reply sends:
ret = {
"pub_key": self.master_key.get_pub_str(), # clean_key'd (450 bytes)
...
}
if self.master_key.pubkey_signature():
ret["pub_sig"] = self.master_key.pubkey_signature() # signed against raw (451 bytes)
The minion then calls verify_pubkey_sig(payload["pub_key"], payload["pub_sig"]) — verifying the signature (computed against 451-byte raw content) against the 450-byte clean_key'd content. The cryptographic signature is invalid because the signed message doesn't match.
In contrast, the dynamic signing path works correctly because it signs the already-cleaned content:
pub_sign = salt.crypt.sign_message(
self.master_key.get_sign_paths()[1],
ret["pub_key"], # <-- already clean_key'd
key_pass,
)
Proof of Concept
Run on master with Salt Python (/opt/saltstack/salt/bin/python3):
import binascii, salt.utils.files, salt.crypt
pub_path = "/etc/salt/pki/master/master.pub"
sig_path = "/etc/salt/pki/master/master_pubkey_signature"
verify_key = "/etc/salt/pki/master/master_sign.pub"
with salt.utils.files.fopen(pub_path) as fp:
raw_pub = fp.read()
cleaned_pub = salt.crypt.clean_key(raw_pub)
with salt.utils.files.fopen(sig_path) as fp:
sig_bytes = binascii.a2b_base64(salt.crypt.clean_key(fp.read()))
print("raw == cleaned:", raw_pub == cleaned_pub)
# False — raw has trailing \n
print("Verify vs raw (what was signed):",
salt.crypt.verify_signature(verify_key, raw_pub, sig_bytes))
# True
print("Verify vs clean_key (what master sends):",
salt.crypt.verify_signature(verify_key, cleaned_pub, sig_bytes))
# False — THIS IS WHAT THE MINION DOES → always fails
Output:
raw == cleaned: False
Verify vs raw (what was signed): True
Verify vs clean_key (what master sends): False
Suggested Fix
In gen_signature(), normalize the pub key content before signing so it matches what get_pub_str() sends:
def gen_signature(priv_path, pub_path, sign_path, passphrase=None):
with salt.utils.files.fopen(pub_path) as fp_:
mpub_64 = clean_key(fp_.read()) # <-- normalize to match get_pub_str()
mpub_sig = sign_message(priv_path, mpub_64, passphrase)
...
After applying this fix, salt-key --gen-signature must be re-run to regenerate master_pubkey_signature.
Versions Affected
- Confirmed on Salt 3006.17 and verified in source on 3006.23 (code is identical)
- Likely affects all 3006.x releases since
clean_key() and gen_signature() have not changed
- PEM trailing newline is inherent to the
cryptography library (tested on 42.0.5 and 46.0.5) and mandated by RFC 7468
Setup to Reproduce
Master config:
master_sign_pubkey: True
master_use_pubkey_signature: True
Generate signing keys:
salt-key --gen-signature --auto-create
Minion config:
verify_master_pubkey_sign: True
Copy master_sign.pub to the minion's pki directory and restart the minion. Signature verification will always fail.
Note
This is a separate issue from #66126 / PR #66140 / #66153 / #66161 which fix newline handling in minion public key comparison during _auth(). This bug is in the reverse direction — master signature verification on the minion side.
Type of salt install
Official deb
Major version
3006.x
What supported OS are you seeing the problem on? Can select multiple. (If bug appears on an unsupported OS, please open a GitHub Discussion instead)
ubuntu-24.04, ubuntu-22.04, rhel-9, rhel-8, amazonlinux-2023
salt --versions-report output
Salt Version:
Salt: 3006.17
Python Version:
Python: 3.10.19 (main, Oct 30 2025, 04:53:28) [GCC 11.2.0]
Dependency Versions:
cffi: 2.0.0
cherrypy: 18.10.0
cryptography: 42.0.5
dateutil: 2.8.1
docker-py: Not Installed
gitdb: 4.0.12
gitpython: 3.1.46
Jinja2: 3.1.6
libgit2: Not Installed
looseversion: 1.0.2
M2Crypto: Not Installed
Mako: Not Installed
msgpack: 1.0.2
msgpack-pure: Not Installed
mysql-python: Not Installed
packaging: 24.0
pycparser: 2.21
pycrypto: Not Installed
pycryptodome: 3.19.1
pygit2: Not Installed
python-gnupg: 0.4.8
PyYAML: 6.0.1
PyZMQ: 23.2.0
relenv: 0.21.2
smmap: 5.0.2
timelib: 0.3.0
Tornado: 4.5.3
ZMQ: 4.3.4
Salt Extensions:
saltext.azurerm: 4.4.0
System Versions:
dist: ubuntu 22.04.5 jammy
locale: utf-8
machine: x86_64
release: 6.17.0-1007-aws
system: Linux
version: Ubuntu 22.04.5 jammy
What happened?
Description
When
master_sign_pubkey: Trueandmaster_use_pubkey_signature: Trueare enabled, minions always fail to verify the pre-computed master public key signature with:Dynamic signing (without
master_use_pubkey_signature) works correctly.Root Cause
gen_signature()insalt/crypt.pysigns the raw file content ofmaster.pub, which includes a trailing newline (\n) added by thecryptographylibrary's PEM encoding (per RFC 7468):However, when the master sends its public key to minions during auth, it uses
get_pub_str()which appliesclean_key():And
clean_key()strips the trailing newline:In
salt/channel/server.py, the auth reply sends:The minion then calls
verify_pubkey_sig(payload["pub_key"], payload["pub_sig"])— verifying the signature (computed against 451-byte raw content) against the 450-byteclean_key'd content. The cryptographic signature is invalid because the signed message doesn't match.In contrast, the dynamic signing path works correctly because it signs the already-cleaned content:
Proof of Concept
Run on master with Salt Python (
/opt/saltstack/salt/bin/python3):Output:
Suggested Fix
In
gen_signature(), normalize the pub key content before signing so it matches whatget_pub_str()sends:After applying this fix,
salt-key --gen-signaturemust be re-run to regeneratemaster_pubkey_signature.Versions Affected
clean_key()andgen_signature()have not changedcryptographylibrary (tested on 42.0.5 and 46.0.5) and mandated by RFC 7468Setup to Reproduce
Master config:
Generate signing keys:
Minion config:
Copy
master_sign.pubto the minion's pki directory and restart the minion. Signature verification will always fail.Note
This is a separate issue from #66126 / PR #66140 / #66153 / #66161 which fix newline handling in minion public key comparison during
_auth(). This bug is in the reverse direction — master signature verification on the minion side.Type of salt install
Official deb
Major version
3006.x
What supported OS are you seeing the problem on? Can select multiple. (If bug appears on an unsupported OS, please open a GitHub Discussion instead)
ubuntu-24.04, ubuntu-22.04, rhel-9, rhel-8, amazonlinux-2023
salt --versions-report output
Salt Version: Salt: 3006.17 Python Version: Python: 3.10.19 (main, Oct 30 2025, 04:53:28) [GCC 11.2.0] Dependency Versions: cffi: 2.0.0 cherrypy: 18.10.0 cryptography: 42.0.5 dateutil: 2.8.1 docker-py: Not Installed gitdb: 4.0.12 gitpython: 3.1.46 Jinja2: 3.1.6 libgit2: Not Installed looseversion: 1.0.2 M2Crypto: Not Installed Mako: Not Installed msgpack: 1.0.2 msgpack-pure: Not Installed mysql-python: Not Installed packaging: 24.0 pycparser: 2.21 pycrypto: Not Installed pycryptodome: 3.19.1 pygit2: Not Installed python-gnupg: 0.4.8 PyYAML: 6.0.1 PyZMQ: 23.2.0 relenv: 0.21.2 smmap: 5.0.2 timelib: 0.3.0 Tornado: 4.5.3 ZMQ: 4.3.4 Salt Extensions: saltext.azurerm: 4.4.0 System Versions: dist: ubuntu 22.04.5 jammy locale: utf-8 machine: x86_64 release: 6.17.0-1007-aws system: Linux version: Ubuntu 22.04.5 jammy