Skip to content

[Bug]: master_use_pubkey_signature: True always fails signature verification #68930

@lubinatien

Description

@lubinatien

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugbroken, incorrect, or confusing behaviorneeds-triage

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions