mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-05 11:08:25 +00:00
Stop claiming the Noise handshake hash provides transcript binding
The published crypto tables named the bare Noise pattern strings without recording that this construction passes an empty associated-data field where standard Noise feeds the handshake hash. The security reference now carries a short deviation subsection stating what that choice does and does not buy: domain separation and DH binding survive through the chaining key, while transcript binding is the property actually absent. The comment on the hash field called it transcript binding and four getters called it channel binding. Nothing in production reads the value, so all five overstated it. They now describe what the field is, and the field comment records that anything built on it will silently not work until the associated data carries the hash.
This commit is contained in:
@@ -223,7 +223,10 @@ than network addresses. A session survives:
|
||||
|
||||
FSP uses Noise XK for session encryption, distinct from the Noise IK
|
||||
pattern used at the link layer. The full Noise descriptor is
|
||||
`Noise_XK_secp256k1_ChaChaPoly_SHA256`.
|
||||
`Noise_XK_secp256k1_ChaChaPoly_SHA256`, with one deviation recorded in
|
||||
[the security reference](../reference/security.md): the handshake AEAD
|
||||
uses an empty associated-data field where standard Noise
|
||||
`EncryptAndHash` uses the handshake hash `h`.
|
||||
|
||||
The XK pattern (pre-message: `← s`):
|
||||
|
||||
|
||||
@@ -58,16 +58,34 @@ idempotent).
|
||||
| Curve | secp256k1 | FMP IK, FSP XK, Schnorr signatures |
|
||||
| Diffie-Hellman | ECDH on secp256k1 (x-only normalized) | Noise IK, Noise XK |
|
||||
| AEAD | ChaCha20-Poly1305 | FMP link encryption, FSP session encryption |
|
||||
| Hash | SHA-256 | NodeAddr derivation, Noise transcript |
|
||||
| Hash | SHA-256 | NodeAddr derivation, Noise key schedule |
|
||||
| Key derivation | HKDF-SHA256 | Noise key schedule |
|
||||
| Signatures | secp256k1 Schnorr | TreeAnnounce, LookupResponse proof, Nostr adverts |
|
||||
| Noise pattern (link) | `Noise_IK_secp256k1_ChaChaPoly_SHA256` | FMP link layer (IK with epoch payload) |
|
||||
| Noise pattern (session) | `Noise_XK_secp256k1_ChaChaPoly_SHA256` | FSP session layer (XK with epoch payload) |
|
||||
| Noise pattern (link) | `Noise_IK_secp256k1_ChaChaPoly_SHA256`, with the deviation below | FMP link layer (IK with epoch payload) |
|
||||
| Noise pattern (session) | `Noise_XK_secp256k1_ChaChaPoly_SHA256`, with the deviation below | FSP session layer (XK with epoch payload) |
|
||||
|
||||
These choices align with the Nostr cryptographic stack
|
||||
(secp256k1 + ChaCha20-Poly1305 + SHA-256) and the NIP-44 encrypted
|
||||
messaging standard.
|
||||
|
||||
### Deviation: Empty Associated Data in the Handshake AEAD
|
||||
|
||||
Both Noise patterns above deviate from the standard construction in one
|
||||
respect. The handshake AEAD uses an empty associated-data field where
|
||||
standard Noise `EncryptAndHash` uses the handshake hash `h`.
|
||||
|
||||
The choice was deliberate. Using secp256k1 rather than 25519 already put the
|
||||
construction outside standard Noise, so no standard-Noise peer could be
|
||||
confused with it, and the transcript hash bought no distinguishing value.
|
||||
|
||||
That argument is about domain separation, and on those grounds it holds. It
|
||||
does not cover transcript binding, which is the property actually absent.
|
||||
Domain separation and DH binding survive through the chaining key `ck`, which
|
||||
`mix_key` chains from `ck = h`, seeded from the protocol name in
|
||||
`SymmetricState::initialize` (`src/noise/handshake.rs`). The handshake hash
|
||||
`h` is maintained at every step and is never fed to the AEAD, so it binds
|
||||
nothing.
|
||||
|
||||
## Rekey Defaults
|
||||
|
||||
Both link-layer and session-layer Noise sessions rekey under one of
|
||||
|
||||
@@ -21,7 +21,13 @@ use std::fmt;
|
||||
struct SymmetricState {
|
||||
/// Chaining key for key derivation.
|
||||
ck: [u8; 32],
|
||||
/// Handshake hash for transcript binding.
|
||||
/// Running SHA-256 accumulator over the handshake transcript.
|
||||
///
|
||||
/// Maintained by `mix_hash` at every step, but never fed to the AEAD:
|
||||
/// `encrypt_and_hash` passes an empty associated-data field. Nothing in
|
||||
/// production reads it, so it provides no transcript binding today, and
|
||||
/// anything built on `handshake_hash()` (channel binding, an exporter,
|
||||
/// cookie binding) will silently not work until the AAD carries `h`.
|
||||
h: [u8; 32],
|
||||
/// Current cipher state for encrypting handshake payloads.
|
||||
cipher: CipherState,
|
||||
@@ -101,7 +107,7 @@ impl SymmetricState {
|
||||
(CipherState::new(k1), CipherState::new(k2))
|
||||
}
|
||||
|
||||
/// Get the handshake hash (for channel binding).
|
||||
/// Get the handshake hash.
|
||||
fn handshake_hash(&self) -> [u8; 32] {
|
||||
self.h
|
||||
}
|
||||
@@ -916,7 +922,7 @@ impl HandshakeState {
|
||||
))
|
||||
}
|
||||
|
||||
/// Get the handshake hash (for channel binding, available after complete).
|
||||
/// Get the handshake hash (available after complete).
|
||||
pub fn handshake_hash(&self) -> [u8; 32] {
|
||||
self.symmetric.handshake_hash()
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ pub struct NoiseSession {
|
||||
send_cipher: CipherState,
|
||||
/// Cipher for receiving.
|
||||
recv_cipher: CipherState,
|
||||
/// Handshake hash for channel binding.
|
||||
/// Handshake hash.
|
||||
handshake_hash: [u8; 32],
|
||||
/// Remote peer's static public key.
|
||||
remote_static: PublicKey,
|
||||
@@ -190,7 +190,7 @@ impl NoiseSession {
|
||||
self.replay_window.reset();
|
||||
}
|
||||
|
||||
/// Get the handshake hash for channel binding.
|
||||
/// Get the handshake hash.
|
||||
pub fn handshake_hash(&self) -> &[u8; 32] {
|
||||
&self.handshake_hash
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user