mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-05 19:18:25 +00:00
Bring the changelog current for the comment, key-clearing and framing work
Thirteen commits had landed since the last changelog update with nothing recorded. Added: the key material clearing and what it deliberately does not cover, the link-handshake identity confirmation behind the address waiver, the frame-length disagreement drop, and the correction to what the Noise handshake hash is claimed to provide. The comment work is one bullet rather than six. None of it changes runtime behaviour, and the only consequence a user can observe is that a comment in the shipped source or the installed DNS setup helper now resolves. Also recorded, and not mentioned by any of the thirteen commit messages: the key-clearing work adds Drop to four public types, which stops their fields being moved out. That is source-breaking for anyone using this crate as a library, most reachably through node.identity on the public config type.
This commit is contained in:
+103
@@ -139,6 +139,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
reports false when the result is zero, and `can_forward` is true only at a
|
||||
TTL of 2 or more.
|
||||
|
||||
- Comments throughout the source tree, the packaging files and the test scripts
|
||||
no longer cite internal identifiers, planning documents or private stage names
|
||||
that a reader of the published tree cannot resolve; each now states the thing
|
||||
the citation stood for. A handful of comments that described behaviour the
|
||||
code does not have — the control-plane read path, its snapshot dispatch, and
|
||||
the MMP report types — have been corrected rather than merely reworded. One of
|
||||
the edited files, the DNS setup helper, installs to `/usr/lib/fips` on every
|
||||
packaging path, so its comment reached users. No code changed.
|
||||
|
||||
- **Source-breaking for consumers of the library crate**: four public types now
|
||||
implement `Drop`, so their fields can no longer be moved out. `Identity`,
|
||||
`ResolvedIdentity`, `IdentityConfig` and `HandshakeState` each gained one as
|
||||
part of clearing key material at end of scope. `IdentityConfig` is the one
|
||||
most likely to be reached in practice, since it hangs off the public `Config`
|
||||
as `node.identity`, so code that moved the nsec out of a configuration value
|
||||
no longer compiles and needs `Option::take` instead. Nothing about the
|
||||
behaviour of the shipped binaries changes; this affects only callers using
|
||||
`fips` as a library.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Inbound session-setup messages are now rate limited, keyed on the
|
||||
@@ -633,6 +652,90 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
claimed source and destination pairing no honest forwarder could produce.
|
||||
The drop log line now carries the signal type and the refusal class.
|
||||
|
||||
- Private and symmetric key material is now cleared when it goes out of scope.
|
||||
Nothing in the crate erased a key before this: the node's private key sat in
|
||||
the loaded configuration in plaintext for the whole process lifetime, which is
|
||||
the longest any secret lives here, every Noise handshake left its static and
|
||||
ephemeral keypairs, its per-message Diffie-Hellman results and its chaining
|
||||
key in freed memory, and each session's ChaCha20-Poly1305 keys were dropped
|
||||
intact. Clearing now covers the retained cipher key on each cipher state, the
|
||||
chaining key and the handshake hash, the 64-byte HKDF outputs and the two
|
||||
session keys derived from them, the static and ephemeral keypairs a handshake
|
||||
holds for its whole duration, the identity's long-term keypair, the temporary
|
||||
copy each of the fourteen elliptic-curve operations makes from a keypair, the
|
||||
bech32 and hex encodings of a secret, and the private key on its way through
|
||||
configuration — including the config file's whole text, which is treated as
|
||||
secret for as long as it is held, since `node.identity.nsec` is read straight
|
||||
out of it. Two places that assigned over an already-loaded key now clear the
|
||||
old value first: assignment frees the previous string without running the
|
||||
type's clearing destructor, so a configuration that already carried a key left
|
||||
the superseded copy in the heap whenever a second source replaced it. **This
|
||||
clears the copies the crate owns, not every copy that ever existed.** The
|
||||
secp256k1 key types are copyable, so the compiler may duplicate them where no
|
||||
code here can name them, which is why that library calls its own erase
|
||||
non-secure. The hash and key-derivation states, and the cipher keys cached
|
||||
inside `ring`, offer no clearing route at the versions pinned here and are
|
||||
deliberately left alone; the residue any of this leaves needs access to the
|
||||
process's memory, or to a core dump or swap image of it, to read. Adds a
|
||||
dependency on `zeroize`. Nothing on the wire and no configuration changes.
|
||||
|
||||
- A link handshake admitted by the established-address waiver is now confirmed
|
||||
against the identity that owns that address, instead of on the address alone.
|
||||
A transport configured with `accept_connections` false still admits an inbound
|
||||
msg1 whose source matches an established peer, so that a peer re-handshaking
|
||||
after a restart or a rekey is not locked out, but nothing checked that the
|
||||
party sourcing from that address was the peer. Any off-path party able to send
|
||||
from it therefore obtained a full link handshake from a node configured to
|
||||
accept none. Once the key exchange reveals the initiator's static key, the
|
||||
handshake is now dropped unless that key belongs to the identity the matched
|
||||
address is attributed to, and dropped as well when the waiver was used and no
|
||||
identity owns the address at all, which fails closed rather than skipping the
|
||||
check for that case. The cheap refusal is unchanged: a stranger reaching a
|
||||
transport that refuses inbound connections is still turned away before any
|
||||
cryptography. Attribution consults both the reverse-address lookup and the
|
||||
scan over established peers rather than stopping at whichever answers first,
|
||||
because the reverse lookup can name a link that no longer exists, and stopping
|
||||
there would refuse a peer the scan can still attribute — permanently, since
|
||||
the confirmation returns above the code that repairs that lookup. Both
|
||||
refusals log at warning level, naming the expected and actual peers, and
|
||||
charge the existing handshake bad-state rejection counter rather than one of
|
||||
their own.
|
||||
|
||||
- An inbound frame whose header disagrees with the frame that arrived is now
|
||||
dropped, at the single dispatch point every transport converges on, before the
|
||||
declared length can be used as a parsing input. The 4-byte common prefix
|
||||
carries a payload length that the node never read, so on the datagram
|
||||
transports — UDP, Ethernet and BLE, which deliver one whole frame per packet
|
||||
and where the arrived length is therefore known exactly — nothing compared the
|
||||
two. This closes no known defect, and it is worth being exact about what it
|
||||
drops on a deployed line. The stream transports (TCP, Tor, Nym) read their
|
||||
frame boundary out of that same field, so the comparison holds by construction
|
||||
and never fires for them. A short datagram is a truncated frame, which already
|
||||
failed the AEAD tag or the exact-size handshake parse, so what changes there
|
||||
is which reason it is dropped for rather than whether it is dropped. A frame
|
||||
whose phase the node does not recognize carries no fixed relationship between
|
||||
the two and is left alone rather than rejected on a guess. The drop takes its
|
||||
own rejection reason and its own `payload_len_mismatch` counter rather than
|
||||
reusing the admission one, since it is a framing rejection decided before the
|
||||
phase dispatch and it applies to established data frames as well as to
|
||||
handshakes; that counter is not yet readable through the control socket.
|
||||
|
||||
- The security reference now records that both Noise patterns deviate from the
|
||||
standard construction in one respect: the handshake AEAD passes an empty
|
||||
associated-data field where standard Noise `EncryptAndHash` uses the handshake
|
||||
hash `h`. The published tables named `Noise_IK_secp256k1_ChaChaPoly_SHA256`
|
||||
and `Noise_XK_secp256k1_ChaChaPoly_SHA256` unqualified, so anyone auditing the
|
||||
stack against the Noise specification had nothing telling them where to look.
|
||||
Domain separation and Diffie-Hellman binding survive through the chaining key,
|
||||
which is seeded from the protocol name and chained at every step; transcript
|
||||
binding is the property actually absent. Nothing in the daemon reads the
|
||||
handshake hash, so no shipped behaviour rests on it, but the comments that
|
||||
called it transcript binding or channel binding overstated it and now describe
|
||||
what the field is, and the field records that anything later built on it —
|
||||
channel binding, an exporter, cookie binding — will silently not work until
|
||||
the associated data carries `h`. This is a correction to what is documented
|
||||
and claimed; no code behaviour and nothing on the wire changed.
|
||||
|
||||
## [0.4.1] - 2026-07-19
|
||||
|
||||
### Changed
|
||||
|
||||
Reference in New Issue
Block a user