# Noise Transport

Node-to-node journal sync over `Noise_IK_25519_ChaChaPoly_SHA256` on `:8443`.
Every connection is mutually authenticated, encrypted, and checked against
fleet membership before a single sync byte flows.

> Name decode: **IK**, initiator knows the responder's key, one round trip;
> **25519**, X25519 Diffie-Hellman; **ChaChaPoly**, ChaCha20-Poly1305 framing;
> **SHA256**, handshake hashing. Broken down in full below.

## Why Noise instead of TLS

TLS carries identity in X.509 certificates, which carry validity windows,
which require wall-clock agreement across the fleet. Concord nodes run
partitioned for weeks with no clock discipline, so the old transport fought
its own framework: custom verification callbacks, disabled session resumption,
and validity windows that were issued but never enforced.

Noise carries identity as a raw static key plus one CA signature over a
parcel binding that key to a node. There is no certificate envelope, so there
is no date field to ignore and no clock to consult. The credential matches
exactly what the system uses, and rotation and revocation become journal rules
instead of certificate machinery.

## The suite name, part by part

`Noise_IK_25519_ChaChaPoly_SHA256` names the full protocol, four parts:

* **IK** is the handshake pattern. The initiator knows the responder's static
  key before connecting, so the handshake finishes in one round trip with
  both sides authenticated. Every dial target arrives with a gossiped key, so
  no other pattern is needed.
* **25519** is the Diffie-Hellman function, X25519 over Curve25519. Both sides
  combine their private key with the other's public key and arrive at the
  same shared secrets, which ratchet into the session keys.
* **ChaChaPoly** is the framing cipher, ChaCha20-Poly1305. Every transport
  message is encrypted and authenticated with it under the session keys.
* **SHA256** is the handshake hash. It drives the key schedule and the running
  transcript hash that both sides compare implicitly as the handshake
  proceeds.

Note what SHA256 here is not: it is not the parcel check. The parcel uses
SHA-256 separately, as one step inside RSA signing described below, and
membership is decided by the RSA signature, never by a hash comparison. The
two uses share an algorithm and nothing else.

Any other combination would be an equally valid suite with its own name, and
our nodes would reject it on first message. Implementation varieties of Noise are large, this page describes what we have exactly.

## Key inventory

The cluster has exactly one CA keypair, created once by the operator and
shared to every node. `ca.crt` carries the CA public key and is storage only:
Concord never performs X.509 verification and never checks dates. `ca.key`
holds the CA private key, which produces one RSA signature per node over that
node's parcel. That signature is the node's membership credential.

Each node generates its own X25519 static keypair on first boot, stored as 32
raw bytes in `noise/secret.key`. The private half never leaves disk. Beside
it sits `noise/generation`, one big-endian number counting that node's key
rotations from zero. A fifth secret, the AES key in
`memberservice/secret.key`, encrypts SWIM gossip and is unrelated to Noise.
No CA material is ever generated at runtime.

## Vocabulary

* **Static key**: the node's long-term X25519 keypair. Holding the private
  half is what the handshake proves.
* **Generation**: the node's key rotation counter. First key is 0, each
  rotation adds one.
* **Parcel**: the session token `(node ID, generation, CA signature)`,
  fixed-width head plus variable-length signature tail. The parcel is the
  membership proof; the static key is the authentication proof. A session
  needs both.
* **Pin**: the first valid key recorded for a node ID, persisted as a
  `peer.keypinned` journal event and projected into the `pinnedkeys` view.
* **Prologue**: a fixed string both sides mix into the handshake hash. Any
  mismatch fails the session, which binds every connection to this exact
  protocol and blocks cross-protocol splicing.

## The parcel, exactly

```
signable message (81 B) --SHA-256 + RSA/ca.key--> CA signature (256 B)
```

Two byte layouts matter. Do not confuse them: the first is what gets signed
at boot, the second is what travels in every session.

The signable message, 81 bytes, built once per boot:

```
 0                   25                  41                  49                 81
 ├───────────────────┼───────────────────┼───────────────────┼──────────────────┤
 │ domain prefix     │ node ID           │ generation number │ static public key│
 │ 25 bytes          │ 16 bytes          │ 8 bytes, big-end. │ 32 bytes         │
 └───────────────────┴───────────────────┴───────────────────┴──────────────────┘
```

The session parcel, 280 bytes with RSA-2048, traveling in each handshake:

```
 0                   16                  24                                280
 ├───────────────────┼───────────────────┼─────────────────────────────────┤
 │ node ID           │ generation number │ CA signature                    │
 │ 16 bytes          │ 8 bytes, big-end. │ 256 bytes                       │
 └───────────────────┴───────────────────┴─────────────────────────────────┘
```

Signing happens once per boot on the node itself; verification happens on
every peer, every session:

```mermaid
flowchart LR
    subgraph Sign["Signer (boot)"]
        S1["domain + ID + gen + pub"] --> S2["SHA-256 hash"] --> S3["RSA sign with ca.key<br/>256 sig bytes"]
    end
    subgraph Verify["Verifier (every session, both sides)"]
        V1["domain + ID + gen + handshake key"] --> V2["SHA-256 hash"] --> V3["RSA verify with ca.crt<br/>accept or reject"]
    end
    S3 -- "compare" --> V3
```

The two layouts are related by the signature: signing the 81-byte message
produces the 256-byte signature embedded in the 280-byte parcel.

The message itself never travels. Only its signature does, alongside the ID
and generation in the clear.

In steps:

1. **Build.** Assemble the 81 bytes: domain, node ID, generation number,
   public key.
2. **Sign.** SHA-256 the bytes, RSA-sign the digest with `ca.key`. The
   signature is over the hash, not over the message directly, because RSA
   signs fixed-size digests.
3. **Travel.** Send the parcel `(ID, generation, signature)` in the handshake
   payload. The public key is already known to the other side from gossip or
   from completing the handshake itself.
4. **Verify.** Rebuild the 81 bytes from the claimed ID, the claimed
   generation number, and the presented public key. Hash, RSA-check against
   `ca.crt`. Accept or reject.

A passing check confirms three facts at once. The presenter holds the static
private key, since only it could complete the handshake. The key is bound to
the claimed node ID at the claimed generation number, since only the CA could
sign that tuple. And the binding is fresh enough to trust, which the pin rules
below decide.

## Boot sequence

1. Ensure the static key, creating `noise/secret.key` on first boot.
2. Ensure the generation counter, creating it at 0 on first boot.
3. Sign the parcel `(node ID, generation, public key)` with `ca.key`.
4. Gossip the public key and generation via memberlist metadata.
5. Serve `:8443` and start dialing peers. The signature travels inside each
   Noise session, never in gossip.

## Gossip

Memberlist metadata carries the Noise public key (32 bytes) and generation
per node, roughly 290 bytes total against memberlist's hard 512-byte limit.
The 256-byte CA signature is excluded for exactly this reason. Gossip itself
is AES-encrypted with the cluster gossip key.

## Session flow

```mermaid
sequenceDiagram
    participant D as Dialer (initiator)
    participant R as Responder
    D->>R: TCP connect to :8443
    D->>R: IK msg1 + parcel (ID, gen, sig)
    Note over R: verify parcel, then reply
    R->>D: IK msg2 + parcel (ID, gen, sig)
    Note over D: verify parcel
    D->>R: HTTP POST /v1/sync (AEAD frames)
    R->>D: HTTP 200 + events (AEAD frames)
    Note over D,R: close, session keys discarded
```

How identity is established, precisely: the dialer encrypts its parcel in the
first handshake message, which only the true responder can read, since reading
it requires the responder's static private key. The responder decodes the
parcel, runs the CA check and the pin check, and only then answers with its
own parcel. The dialer decodes that parcel, confirms it names the dialed node
ID, and runs the same two checks. Only then does HTTP start. Neither side
sends anything readable to an unverified peer: the responder sends no reply
bytes before verifying, and the dialer sends no sync bytes before verifying.

After the handshake, sync runs as plain HTTP/1.1 over the encrypted channel:
same `POST /v1/sync`, same watermark cursors, same idempotent apply. One
connection carries one sync; session keys are discarded on close, so recorded
traffic cannot be decrypted later from a stolen static key.

## Verification rules

Every handshake runs both checks, in both directions, in this order:

1. **CA signature.** Rebuild the parcel, hash it, verify the RSA signature.
   Failure means non-member, rejected. No clocks, no chains, no dates.
2. **Pin table.** Look up the node ID among pinned keys.
   * No pin: first sight. Record the pin in the journal, admit.
   * Higher generation than pinned: rotation. Re-pin, admit.
   * Same generation, different key: reject with both keys in the log.
     The pin sticks.
   * Lower generation: stale, reject.
   * Exact match: admit. One view read, nothing recorded.

The responder additionally requires the presented static key to equal the
bytes the CA signed, and the dialer requires the responder to prove the
dialed node ID, so cursors cannot be poisoned by a confused peer.

## Rotation procedure

1. Delete `noise/secret.key`.
2. Bump `noise/generation` by one.
3. Restart the node. Boot generates a fresh key, re-signs, gossips the new
   generation, and peers re-pin on first contact.

Recovery from a pin alarm is always a bump: it is an intentional act, and the
higher generation supersedes every older pin fleet-wide with no coordination.

## Threat model

* A passive observer sees TCP to `:8443` and nothing else. Handshake messages
  hide static keys, payloads are encrypted, gossip is AES-sealed.
* An active attacker without keys passes neither check and never sees sync
  bytes.
* A stolen static key impersonates its node until rotation, and the pin alarm
  fires the moment the legitimate key is also heard.
* A stolen `ca.key` mints arbitrary members. Nothing contains this: every
  node holds `ca.key` by provisioning choice, so CA compromise is fleet
  compromise under any transport.
* Partial state loss (new key at an old generation with a valid signature)
  is rejected by the pin and healed by a bump, instead of silently splitting
  the fleet.

## Non-goals

* No XX handshake: every dial target arrives with a gossiped key, so the
  first-contact pattern has no trigger in this architecture.
* No certificate handling: `ca.crt` is storage for one public key, and the
  node certificate minted beside it is unused by the transport.
* Revocation beyond pinning (explicit deny lists) plugs into the verifier
  seam and is not implemented.
