Spindle is a cryptographic library. Bugs here are security bugs by default, so please read SPEC.md — particularly §2 (threat model) and §8 (residual risks) — before reporting, since several properties people expect are explicitly out of scope rather than broken.
| Version | Supported |
|---|---|
| 0.1.x | ✅ |
| < 0.1 | ❌ |
The API is not yet stable. While the module is v0.x, security fixes land on the
latest minor only — there is no backport branch.
- Open a public issue
- Post in discussions
- Share details publicly before a patch is released
- Email: rex@xraph.com, or open a GitHub Security Advisory on this repository.
- Provide details:
- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (if any)
- Your contact information
- Initial Response: Within 48 hours
- Status Update: Within 5 business days
- Fix Timeline: Depends on severity
- Critical: 1-7 days
- High: 7-14 days
- Medium: 14-30 days
- Low: 30-90 days
- Vulnerability reported and confirmed
- Patch developed and tested
- Security advisory drafted
- Patch released
- Public disclosure (coordinated)
We follow a 90-day disclosure timeline unless a critical severity warrants faster action, coordination with other projects is needed, or circumstances are otherwise exceptional.
A report is in scope if it shows any of the following:
- A way to read or forge a command without holding an endpoint's private key.
- A handshake completing against a peer whose static key was not the pinned one.
- A replayed frame being accepted, or a fresh frame being wrongly rejected as a replay.
- Two different deployment scopes producing the same prologue, or two endpoints deriving different bindings from the same handshake.
- An attestation from one session verifying in another.
- Any path that reuses a
(key, nonce)pair, or that advances a sequence number pastMaxSeqrather than erroring. - A commissioning fingerprint that fails to change when a key is substituted.
These are documented properties, not defects. Reporting them is welcome as discussion, but they will not be treated as vulnerabilities:
- Availability. A broker that drops or delays frames denies service. Spindle detects this; it cannot prevent it.
- Traffic analysis. Frame sizes and timing are visible to the broker. There is no padding or cover traffic.
- A commissioning-mode device accepting any controller. That is what
commissioning mode is for. It is guarded by physical operator presence and the
fingerprint comparison, and it is explicitly opt-in
(
DeviceConfig.Commissioning). - Compromise of a stored private key. Key storage is the caller's responsibility; Spindle never persists key material.
- No formal model. The Noise pattern is verified upstream, but the composition around it is not. This is stated in SPEC.md §8 and is the largest known open item — a proof, or a counter-example, is very welcome as a contribution.
The controller pins the device static at commissioning; the device pins the
controller static. An empty ControllerStatic is an error, never an implicit
"accept anything". If you find yourself setting Commissioning: true outside a
physically supervised provisioning window, stop — that suspends the only check
that says which controller a device will obey.
Sequence numbers drive the receiving nonce directly. Three rules make that safe, and all three are tested:
- A frame failing authentication never marks its sequence as seen.
- Sequence 0 is a valid first frame.
- Sequence exhaustion returns
ErrSeqExhausted; it never wraps.
Any change to session.go or replay.go deserves review out of proportion to
its size.
Derive one AAD(label) per class of frame and use it consistently on both ends.
Sharing a label across classes lets a captured frame of one kind be accepted as
another.
Pass a real deployment scope. It goes into the Noise prologue, so a device provisioned for one scope cannot complete a handshake for another. An empty scope is legitimate only during commissioning, when the device does not yet know where it belongs.
There is no in-band key rotation, deliberately — an in-band recovery path is an in-band takeover path. Revoke and re-provision.
govulncheck runs in CI against flynn/noise and golang.org/x/crypto. A
vulnerability in either is a vulnerability in Spindle.
The test suite covers both handshake roles, scope and pinning rejection, replay and out-of-order delivery, forged-frame window behaviour, sequence exhaustion, attestation tampering, and wordlist completeness.
go test -race ./...
golangci-lint run # includes gosec
govulncheck ./...CI runs all three on every push and pull request.
For anything that is not a vulnerability report, open a discussion or an issue. For anything that might be one, email rex@xraph.com first.