Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

hashsigs

Hash-based signatures for Python: SHRINCS and SPHINCS+C, backed by the audited hashsigs-rs crate.

SHRINCS is a two-path construction. One committed key carries a cheap bounded stateful path for normal use, and an expensive unbounded stateless path for recovery and rotation. Verification is pure hashing and needs no signer state.

Install

pip install hashsigs

Wheels target CPython 3.9 and later through the stable ABI, so one wheel per platform serves every supported version.

Quick start

import hashlib
import secrets

from hashsigs import shrincs, shrincs_keys_to_secret_bytes

# The seed is REQUIRED and must be 32 bytes. This package pulls in no random
# number generator, so you supply the entropy.
keys = shrincs.keygen(secrets.token_bytes(32), max_signatures=1024)

# Sign a 32-byte message. Pre-hash whatever you are actually signing.
message = hashlib.sha256(b"transfer 10 to alice").digest()
signature = shrincs.sign(message, keys)

assert shrincs.verify(signature, message, keys.public_key_commitment)

# Persist after every stateful signature. See "Stateful signing" below.
with open("key.bin", "wb") as handle:
    handle.write(shrincs_keys_to_secret_bytes(keys))

A verifier stores only the 32-byte public_key_commitment. The signature carries the full public key, and verification checks that key hashes to the commitment before trusting it.

Profiles

This one distribution carries every SHRINCS profile. Each has its own module, and the package root is the default profile, 256s-keccak:

# The default profile.
from hashsigs import shrincs

# Any other profile, from its own module.
from hashsigs.profiles import p128s_q18
Module Profile Scheme hash
hashsigs (root) shrincs-256s-keccak keccak-256
hashsigs.profiles.p256s shrincs-256s-keccak keccak-256
hashsigs.profiles.p256s_sha2 shrincs-256s-sha2 SHA-256
hashsigs.profiles.p128s_q18 shrincs-128s-q18-keccak keccak-256
hashsigs.profiles.p128s_q20 shrincs-128s-q20-keccak keccak-256
hashsigs.profiles.p128s_q18_sha2 shrincs-128s-q18-sha2 SHA-256
hashsigs.profiles.p128s_q20_sha2 shrincs-128s-q20-sha2 SHA-256

Every module exposes the same names. They differ only in what they compute, and a signature made under one profile does not verify under any other. Pick one profile per key and keep it. Persisted key bytes carry no profile tag, so importing them under the wrong profile raises ERR_IMPORT_INVALID rather than producing a key that signs unverifiably.

Read the loaded profile back from the extension itself, not from the import path:

from hashsigs.profiles import p128s_q18

assert p128s_q18.profile_name == "shrincs-128s-q18-keccak"

Choose on cost. The 128s profiles produce much smaller signatures and verify faster, at the price of slow signing. Keygen at 128s takes tens of seconds, against roughly 0.1 seconds at 256s.

The sha2 variants sign about three times faster than their keccak twins. They cost more gas on-chain, though, because keccak is an EVM opcode while SHA-256 is a precompile. The repository README carries the measured table.

Stateful signing

shrincs.sign consumes one leaf of a fixed budget and advances the key. This is the part that needs care.

keys = shrincs.keygen(seed, max_signatures=4)
print(keys.stateful.remaining)  # 4

signature = shrincs.sign(message, keys)
print(keys.stateful.remaining)  # 3

# Persist NOW, before the next signature.
with open("key.bin", "wb") as handle:
    handle.write(shrincs_keys_to_secret_bytes(keys))

Save the key after every stateful signature. Signing twice from the same saved state reuses a one-time leaf, which breaks the guarantee the stateful path rests on and can expose that leaf's secret material. Restore with shrincs.import_signing_key, which revalidates the key against its own seeds and preserves the leaf counter:

with open("key.bin", "rb") as handle:
    keys = shrincs.import_signing_key(handle.read())
print(keys.stateful.remaining)  # 3, not 4

max_signatures is fixed at keygen and cannot be raised later. Once the budget is spent, stateful signing raises:

from hashsigs import HashSigsError

try:
    shrincs.sign(message, keys)
except HashSigsError as err:
    if err.code == "ERR_STATEFUL_LEAVES_EXHAUSTED":
        ...  # rotate, or fall back to the stateless path

The stateless path

shrincs.sign_stateless consumes no leaf, never modifies the key, and works on an exhausted key. It is far slower and its signatures are far larger, so it serves recovery and rotation rather than normal traffic.

signature = shrincs.sign_stateless(message, keys)
assert shrincs.verify_stateless(signature, message, keys.stateless.public_key)

shrincs.reset(keys, new_seed) starts a fresh stateful chain after suspected leaf reuse. The stateless half and max_signatures survive, but public_key_commitment changes, so anything pinning the old commitment stops accepting the key.

Errors

Every failure raises HashSigsError with a stable code. Branch on the code, never on the message text. Messages never echo the input that caused them, because that input is routinely secret key material and messages routinely reach logs.

Code Meaning
ERR_BAD_LENGTH A fixed-width argument had the wrong length
ERR_INVALID_INPUT max_signatures outside 1 to 4096
ERR_KEYGEN_FAILED Key derivation failed for these inputs
ERR_IMPORT_INVALID Recomputed roots do not match the seeds, or the counter is out of range
ERR_STATEFUL_LEAVES_EXHAUSTED Every stateful leaf is spent
ERR_SIGNING_FAILED Grinding failed for this key and message
ERR_ENVELOPE_MALFORMED The signature is not a well-formed envelope

Verification never raises. A malformed signature, a wrong-length key, or a commitment mismatch is simply False.

Building from source

pip install build
python -m build          # wheel + sdist
pip install dist/*.whl

Or straight from a checkout:

pip install .

Both go through py/hashsigs_build.py, a PEP 517 backend that compiles one extension per profile and stages them into the package before handing off to maturin. Use it rather than calling maturin directly: maturin builds one Cargo package, and a bare maturin build produces a wheel whose hashsigs._ext is empty.

Installing from source works the same way. pip install hashsigs --no-binary hashsigs runs that backend from the unpacked sdist and rebuilds every extension, so it needs a Rust toolchain.

The wheel carries one compiled extension per profile under hashsigs._ext, so importing a profile maps only that profile's code. Each extension links its own copy of the crate, at about 715 KB each, so the six add roughly 4.3 MB against 715 KB for a single-profile build. The released wheel is about 2.0 MB compressed. That is the trade this layout makes, and unlike a browser bundle there is no per-import transfer saving to offset it.

License

AGPL-3.0-or-later. See COPYING.

Metadata

Release files for hashsigs 0.2.1rc6

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for hashsigs 0.2.1rc6
File Size Uploaded
hashsigs-0.2.1rc6.tar.gz 3.2 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for hashsigs 0.2.1rc6
File Interpreter ABI Platform
hashsigs-0.2.1rc6-cp39-abi3-manylinux_2_34_x86_64.whl CPython 3.9 abi3 Linux glibc 2.34+ x86-64 Details

Total release size: 5.3 MB

Release files / hashsigs-0.2.1rc6.tar.gz

Download URL hashsigs-0.2.1rc6.tar.gz
Size 3.2 MB
Tags Source
SHA-256 checksum
How to use checksums
5fa9d7609dd11015ccc5fdabec7cc0f653cb0823938403a95652aee6d4301009
BLAKE2b-256 checksum
How to use checksums
db2c0dca263fcba94a218681f4447e30eacb9714054ae6de41066becb194cc6d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.5

Release files / hashsigs-0.2.1rc6-cp39-abi3-manylinux_2_34_x86_64.whl

Download URL hashsigs-0.2.1rc6-cp39-abi3-manylinux_2_34_x86_64.whl
Size 2.0 MB
Tags CPython 3.9 Linux glibc 2.34+ x86-64 abi3
SHA-256 checksum
How to use checksums
01c0eed6cd1bbd5462e06773ae62a81b8be080d3da1a182ea7611d3234dd9aee
BLAKE2b-256 checksum
How to use checksums
156aa5a689195a6727b1f8a8640208b13a2c3a2a016cd39e2be2312b3b1d9511
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.5
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page