This document describes the security considerations for the hashsigs-rs project.
CI runs cargo deny check (the supply-chain job), which scans Cargo.lock against the RustSec advisory database. The policy lives in deny.toml. The list below is current as of 2026-08-20.
The following security advisories affect transitive dependencies from the Solana ecosystem that the project cannot directly control:
- Affected crate:
curve25519-dalek@3.2.0 - Position: dev-dependencies only, through
ed25519-dalek@1.0.1undersolana-program-test(the validator test harness) - Risk assessment: low
- Description: a timing side channel in scalar subtraction. Exploitation requires local access and precise timing measurements.
- Mitigation: the affected version never ships. The library, the wasm package, and the deployed Solana program build with the patched
curve25519-dalek@4.1.3.
- Affected crate:
ed25519-dalek@1.0.1 - Position: dev-dependencies only, under
solana-program-test(the validator test harness) - Risk assessment: low
- Description: the attack requires signatures on crafted messages from the affected signer.
- Mitigation: the affected version never ships, and this project signs nothing with ed25519.
The RustSec database lists the following transitive crates as unmaintained. None is an active vulnerability:
- RUSTSEC-2025-0141:
bincode@1.3.3is unmaintained. The Solana program dependency graph reaches it throughagave-precompiles, sodeny.tomlcarries a dated ignore entry. - RUSTSEC-2024-0388:
derivativeis unmaintained. Dev-dependencies only (solana-program-test). - RUSTSEC-2024-0436:
pasteis unmaintained. Dev-dependencies only (solana-program-test). - RUSTSEC-2025-0161:
libsecp256k1is unmaintained. Dev-dependencies only (solana-program-test).cargo deny checkscopes unmaintained advisories to direct dependencies, so this entry needs nodeny.tomlignore.
These are transitive dependencies from Solana and represent maintenance concerns rather than active security vulnerabilities.
Replacement is not practical. The crates are transitive dependencies from Solana, and the maintained alternatives have incompatible APIs.
-
derivative: maintained alternatives exist (derive_more,educe,derive-where), but they have different APIs and feature sets. The Solana dependency tree usesderivativedeep inside for specific derive macros. -
paste: this proc-macro crate does token pasting in macros. The advisory flags it as unmaintained, but the crate still works. No drop-in replacement with an identical API exists. -
bincode:agave-precompilesuses the 1.x line. The maintained 2.x line has a different API, so a[patch]substitution cannot work.
Why patching fails:
- These crates come from Solana's transitive dependencies, not this project's direct dependencies
- API differences between original and replacement crates prevent simple substitution
- Cargo's patch system requires identical APIs for successful replacement
Mitigation:
- Track Solana's progress on updated dependencies
- The security risk is minimal because these are maintenance warnings, not active vulnerabilities
- Dev-dependencies never appear in published artifacts. The crate, the wasm package, and the deployed program build only from normal dependencies.
-
Regular audits:
cargo auditruns as part of the CI/CD pipeline to catch new vulnerabilities. -
Dependency updates: the project updates dependencies to their latest secure versions where possible.
-
Advisory tracking: the project tracks security advisories for the Solana ecosystem and updates dependencies as soon as secure versions become available.
-
Risk assessment: the project assesses each identified vulnerability for its practical impact on this specific use case.
This repository now contains more than low-level hash-based signature primitives. It includes:
- the standalone
wotsplusmodule - core
shrincssigner / verifier primitives - a
wasmexport surface for JS/TS consumers
Those layers have different security responsibilities. The most important rule for integrators is:
- low-level signature validity is not the same thing as replay protection or production-safe authorization
The core SHRINCS verifier exposes raw, low-level verification of exact caller-supplied message bytes.
Security implication:
- these paths check cryptographic correctness only
- they do not provide freshness, nonce management, replay protection, or policy enforcement on their own
Guidance:
- the calling system owns freshness, nonce management, and replay state
- this library verifies signatures and nothing more
- build domain separation, nonces, and expiry into the message you hash before signing. The library cannot enforce them for you
Signatures from this crate carry no replay protection and no freshness
guarantee. sign produces a signature over exactly the 32-byte digest it
receives, and verify checks exactly that signature against exactly that
digest. Neither call knows whether anyone signed or verified the digest
before.
Integrators must build their own:
- nonce or sequence-number tracking
- domain separation (chain ID, contract address, action type, or a comparable value, folded into the digest before signing)
- expiry or freshness windows, if the use case needs them
For the stateful SHRINCS path, replay resistance also depends on leaf-use discipline. The caller must persist key state and never resubmit a signature produced by a leaf already consumed. See Stateful signing must not reuse leaves below.
Guidance:
- do not assume this library enforces nonces, sequence numbers, or domain separation. It does not
- design the signed message, the 32-byte digest, to carry whatever freshness
and replay-prevention data your app needs before it reaches
sign
keygen and reset require a caller-supplied 32-byte seed. The library has
no RNG fallback and does not check seed quality.
Security implication:
- a weak or predictable seed produces a weak key. The library cannot detect this and derives a key from it regardless
- there is no library-side entropy source to fall back on if the caller supplies bad input
Guidance:
- generate the seed with a cryptographically secure source:
crypto.getRandomValuesin the browser,crypto.randomBytes/webcrypto in Node, or the platform CSPRNG in other host environments - never derive a seed from predictable input such as a counter, timestamp, or user-supplied password without a proper key-derivation function
- the same rule applies to the seed passed to
reset
The stateful SHRINCS path depends on one-time leaf use. Each sign() call
consumes one leaf and advances the in-memory key state. Signing twice from
the same state reuses a leaf, breaks the one-time-signature security the
scheme depends on, and can expose enough of the secret key to forge further
signatures under that leaf.
Security implication:
- signing from a stale copy of the key state (a clone, a snapshot taken
before an earlier
sign()call, or a value not yet written back after a crash) causes a leaf reuse - the library enforces exhaustion: once the leaf budget runs out,
sign()throws instead of reusing a leaf
Guidance:
- persist the current key state after every stateful
sign()call, before you use the signature for anything. A crash between signing and persisting is exactly the window that causes reuse on restart - never sign again from a snapshot or clone taken before a later
sign()call succeeded - once the stateful budget runs out, switch to the stateless path or call
resetwith a fresh seed. Do not work around the exhaustion error
Persisted-state rollback:
- the persisted secret carries the leaf counter, but
importaccepts any in-range counter. It cannot distinguish a current key from an older serialized snapshot restored from a backup. Restoring an older persisted copy and signing re-consumes already-used leaves: the same catastrophic reuse as signing from a stale in-memory clone. - a stateless library owns no persistent state, so it cannot enforce
anti-rollback on its own. The caller must persist a monotonic high-water
mark for the leaf index and refuse to load or sign below it. The Solana
example program (
solana/examples/shrincs-account) enforces exactly this on-chain: its account state rejects a non-monotonic leaf index withStatefulIndexRollback. Treat that program as the reference for the guarantee off-chain callers must provide themselves.
The current SHRINCS design uses a fixed public-key model tied together by
public_key_commitment.
Security implication:
- verification depends on correctly binding:
stateful_public_keypk_seedhypertree_root
- callers must not treat those components as independently swappable fields
Guidance:
- always verify against the installed/original public key bundle
- do not reintroduce message-specific replacement public keys
- treat
public_key_commitmentas the installed key's identifier for every verification call
The WASM layer exposes a single noble-style signing/verification surface
(sphincsPlusC/shrincs, from loadHashSigs()). No higher-level,
policy-enforcing wrapper exists.
Security implication:
sphincsPlusC.sign()/verify()andshrincs.sign()/verify()apply no freshness, replay, or authorization checks. They sign and verify exactly the 32-byte digest they receive- an integration invites misuse when it treats a valid signature alone as proof of authorization and keeps no freshness or replay state of its own
Guidance:
- build any authorization, freshness, or replay logic your app needs in the calling code, and fold the relevant context into the digest before signing: nonce, domain, action type, or whatever the use case requires
- see Freshness and replay protection are the caller's responsibility
The SHRINCS verifier uses ordinary short-circuit equality (==) and early
return false on failed structural and root checks. It does not use
constant-time comparison (such as subtle::ConstantTimeEq) for
public-key commitment, hypertree root, or intermediate hash equality.
Threat-model assumption:
- verification is not assumed to resist a local timing adversary on the host
that can measure sub-operation latency of
verify*with chosen signatures - remote network timing of full verification requests is outside the intended attacker model for this crate. Deployments that face that threat should treat this as residual risk and add their own defenses if needed
Future work, not implemented: constant-time equality on the final root and commitment checks, or a documented constant-time verification profile.
Treat the wasm signer surface as code that runs inside the browser's normal same-origin trust boundary, not inside a hardened enclave.
Security implication:
- secret key material is a set of plain
Uint8Arrayfields the caller holds directly:keys.secret.skSeed/prfSeedfor SPHINCS+C, andkeys.stateful.secret.skSeed/prfSeedpluskeys.stateless.secret.skSeed/prfSeedfor SHRINCS, all returned bysphincsPlusC.keygen()/shrincs.keygen()(or reconstructed byshrincsImportSigningKey) - any XSS, malicious same-origin script, compromised front-end dependency, or hostile extension able to run in the page context can read those fields directly from JS
Guidance:
- do not run the browser signer in pages that execute untrusted third-party JS
- treat browser local storage, IndexedDB, and ordinary JS heap state as a soft boundary, not a strong secret store
- for SHRINCS stateful signing, persist and reuse the same
keysobject.shrincs.sign()mutateskeys.statefulin place on every call. Never sign from a clone or a snapshot taken before an earliersign()call, or you reuse a one-time leaf and break the signature's security - zero each secret field (
skSeed.fill(0),prfSeed.fill(0)) once the key material is no longer needed
The standalone wotsplus module is the legacy v1 scheme: do not use it for
new integrations. Use SHRINCS. It remains only to keep v1 wallets
verifiable.
The module still includes length-sensitive code paths that assume valid message sizing.
Security implication:
- this is primarily a robustness / DoS concern rather than a known signature forgery issue
Guidance:
- check the length of untrusted messages in the caller before you pass them to a low-level WOTS+ API
- treat the WOTS+ module as a low-level primitive surface that enforces no policy
To report a security vulnerability in this project, send a private email to:
security at quip.network
For sensitive security reports, encrypt your message with the project PGP key (last updated 2024-11-14):
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: ProtonMail
xjMEaJt7+hYJKwYBBAHaRw8BAQdAYjy3Rqa6cdJsK1keoMTmfj1clsryEvQi
vEjaqTEa2xfNLXNlY3VyaXR5QHF1aXAubmV0d29yayA8c2VjdXJpdHlAcXVp
cC5uZXR3b3JrPsLAEQQTFgoAgwWCaJt7+gMLCQcJkNiLJOxMcokIRRQAAAAA
ABwAIHNhbHRAbm90YXRpb25zLm9wZW5wZ3Bqcy5vcmee0QrLmO7tOgWYl29h
GqHifldyZ2WPGmsc8ySr2ATCKAMVCggEFgACAQIZAQKbAwIeARYhBHNRtQVW
BVD7YgIrYdiLJOxMcokIAADj/wD+O85VPvR+Nblf+ooEgMQem8qRYNxBhUaP
1lyMSmoV3XgBAPi20j/UC4yfC0ZnfYtV058zfE7BST2q7aNvLY3T+qoBwsAe
BBAWCACQBYJom3w3BYMA7U4ACRDYBsGvWXjoxzUUAAAAAAAcABBzYWx0QG5v
dGF0aW9ucy5vcGVucGdwanMub3JnlhQSiICxkiypXOcKcTzkVywcb3BlbnBn
cC1jYUBwcm90b24ubWUgPG9wZW5wZ3AtY2FAcHJvdG9uLm1lPhYhBAqGUv5d
UzhgV4mf6dgGwa9ZeOjHAABzowD+MYKxGoCzLbl7U0Jd6/ZSZSwPXvWKJjpf
7JiYELMXm7IBANGVF5Mxgj8LA8LqNh6y0TxS14MqYRRk1jQNISLO6+0AzjgE
aJt7+hIKKwYBBAGXVQEFAQEHQG7ytnodbovlbtXvc6klzyGPtnVRPJ6EyiKE
4gxeC/l0AwEIB8K+BBgWCgBwBYJom3v6CZDYiyTsTHKJCEUUAAAAAAAcACBz
YWx0QG5vdGF0aW9ucy5vcGVucGdwanMub3Jnt3zwK9JEzu9mrN5lajCGqt/I
ULIIwaKSgecqmFTtaoMCmwwWIQRzUbUFVgVQ+2ICK2HYiyTsTHKJCAAAA2UB
AK9+2eIPYiWJNt5kMaBYcx6dbjU7C62u2/86sw1DLArJAP9CK/C1LoTovZ89
pW7gWQHbPY6BA6dzdWbnxsDDY/fjBQ==
=SWOf
-----END PGP PUBLIC KEY BLOCK-----
Fingerprint: 7351 B505 5605 50FB 6202 2B61 D88B 24EC 4C72 8908
# Download from ProtonMail's key server
curl -s "https://api.protonmail.ch/pks/lookup?op=get&search=security@quip.network" | gpg --import
# Or download from a public key server
gpg --keyserver keyserver.ubuntu.com --recv-keys 0xD88B24EC4C728908
# Verify the key fingerprint matches the one listed above
gpg --fingerprint security@quip.networkVerify that the key fingerprint matches the fingerprint listed in this document before you encrypt sensitive information.
- Description of the vulnerability
- Steps to reproduce the issue
- Potential impact assessment
- Any suggested fixes or mitigations
The maintainers acknowledge receipt of your report within 48 hours and send a detailed response within 7 days.
Do not create public issues for security vulnerabilities.
The audit configuration in .cargo/audit.toml documents all known issues that the project has assessed and chosen to ignore until upstream fixes arrive.
To run the security audit yourself:
cargo auditThe audit uses this configuration and shows only new, unaddressed security issues.