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
- Re-generate signers_sign with the site's private key over the exact string '<signs_required>:<signer1>,<signer2>' and update content.json
- Confirm the site private key (data/<site>/private.key or users.json site entry) matches the site address
- Ensure signs_required and the valid_signers list are exactly what was signed (order and formatting matter — comma-joined, no spaces)
- 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
- Regenerate signers_sign every time signers or signs_required changes
- Sign with the site's own private key, not a personal one
- Keep the signer list order/format exactly as signed
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
- Valid signs: %s/%s
- Invalid old-style sign
- Includes not allowed
- Invalid json file: %s
- We have newer (Our: %s, Sent: %s)
AI-assisted analysis of HelloZeroNet/ZeroNet@454c0b2e7e (2026-09-02).
Data as JSON: /api/errors/e4bcbc21db423bb1.
Report an issue: GitHub.