HelloZeroNet/ZeroNet · critical · VerifyError

Invalid signers_sign!

Error message

Invalid signers_sign!

What it means

verifyFile raises VerifyError('Invalid signers_sign!') when the root content.json has multiple valid signers (len(valid_signers) > 1) and the 'signers_sign' field fails CryptBitcoin.verify against the site address. signers_sign is a signature (by the site's private key) over the string '<signs_required>:<signer1>,<signer2>,...' proving the owner authorized that signer list and required-signature count. A mismatch means the signer list or signs_required was tampered with, or signed with the wrong key.

Source

Thrown at src/Content/ContentManager.py:986

                sign_content = json.dumps(new_content, sort_keys=True)  # Dump the json to string to remove whitepsace

                # Fix float representation error on Android
                modified = new_content["modified"]
                if config.fix_float_decimals and type(modified) is float and not str(modified).endswith(".0"):
                    modified_fixed = "{:.6f}".format(modified).strip("0.")
                    sign_content = sign_content.replace(
                        '"modified": %s' % repr(modified),
                        '"modified": %s' % modified_fixed
                    )

                if signs:  # New style signing
                    valid_signers = self.getValidSigners(inner_path, new_content)
                    signs_required = self.getSignsRequired(inner_path, new_content)

                    if inner_path == "content.json" and len(valid_signers) > 1:  # Check signers_sign on root content.json
                        signers_data = "%s:%s" % (signs_required, ",".join(valid_signers))
                        if not CryptBitcoin.verify(signers_data, self.site.address, new_content["signers_sign"]):
                            raise VerifyError("Invalid signers_sign!")

                    if inner_path != "content.json" and not self.verifyCert(inner_path, new_content):  # Check if cert valid
                        raise VerifyError("Invalid cert!")

                    valid_signs = 0
                    for address in valid_signers:
                        if address in signs:
                            valid_signs += CryptBitcoin.verify(sign_content, address, signs[address])
                        if valid_signs >= signs_required:
                            break  # Break if we has enough signs
                    if valid_signs < signs_required:
                        raise VerifyError("Valid signs: %s/%s" % (valid_signs, signs_required))
                    else:
                        return self.verifyContent(inner_path, new_content)
                else:  # Old style signing
                    raise VerifyError("Invalid old-style sign")

            except Exception as err:

View on GitHub (pinned to 454c0b2e7e)

Solutions

  1. Re-generate signers_sign with the site's private key over the exact string '<signs_required>:<signer1>,<signer2>' and update content.json
  2. Confirm the site private key (data/<site>/private.key or users.json site entry) matches the site address
  3. Ensure signs_required and the valid_signers list are exactly what was signed (order and formatting matter — comma-joined, no spaces)
  4. If multi-sig is not needed, revert to a single signer so the signers_sign check is skipped

Example fix

// before: edited signers without re-signing
{"signers_sign": "OLD_SIG", "sign": {"1Old": "sig"}}
// after: regenerate signers_sign from current signer list
from Crypt import CryptBitcoin
signers_data = "%s:%s" % (signs_required, ",".join(valid_signers))
content["signers_sign"] = CryptBitcoin.sign(signers_data, privatekey)
Defensive patterns

Strategy: validation

Validate before calling

from Crypt import CryptBitcoin
content = json.load(open('content.json'))
valid_signers = content_manager.getValidSigners('content.json', content)
if len(valid_signers) > 1:
    data = "%s:%s" % (content_manager.getSignsRequired('content.json', content), ','.join(valid_signers))
    assert CryptBitcoin.verify(data, site_address, content['signers_sign']), 'signers_sign mismatch — re-sign'

Try / catch

from Content.ContentManager import VerifyError
try:
    site.content_manager.isModified('content.json', file)
except VerifyError as e:
    if str(e) == 'Invalid signers_sign!':
        resign_signers_sign(site_privatekey, content)
    else:
        raise

Prevention

When it happens

Trigger: verifyFile on inner_path == 'content.json' with getValidSigners returning more than one signer; CryptBitcoin.verify('%s:%s' % (signs_required, ','.join(valid_signers)), site_address, content['signers_sign']) returns False — e.g. after editing signers or signs_required without re-generating signers_sign.

Common situations: Hand-editing valid_signers or signs_required in content.json without re-signing; using a private key that doesn't match the site address; multi-sig sites (shared sites like blogs-with-cert authors) where signers_sign was generated from a different signer set.

Related errors


AI-assisted analysis of HelloZeroNet/ZeroNet@454c0b2e7e (2026-09-02). Data as JSON: /api/errors/e4bcbc21db423bb1. Report an issue: GitHub.