March Docs

ClusterAuth

ClusterAuth: how a node proves it belongs to the cluster in the net-kernel handshake, and the key material the handshake derives.

Two modes (specs/lang/clustering.md, "Authentication & Handshake"):

shared secret  every node holds one secret (MARCH_CLUSTER_SECRET). The
               challenger sends a random nonce; the responder returns
               HMAC(secret, nonce); the secret never goes on the wire.
certificate    every node holds an ed25519 key and a certificate for it
               signed by the operator key (NodeCert; MARCH_NODE_KEY,
               MARCH_NODE_CERT, MARCH_CLUSTER_OPERATOR_PUBKEY). Each side
               verifies the other's certificate and then signs the peer's
               nonce together with the handshake transcript.

Either way the handshake ends with a per-connection MAC key pair (frame_keys): HKDF-SHA256 over the handshake transcript, keyed in certificate mode by an X25519 agreement between ephemeral keys each side sent in its (signed) hello, and in shared-secret mode by the secret.

Types

typeAuthAuth = Secret(String) | Certified(NodeCert.Credentials)#
typeFrameKeysFrameKeys = { tx_key : Bytes, rx_key : Bytes }#

One direction's MAC key: the sender's.

Functions

fncert_proof_messagecert_proof_message(challenge : String, th : Bytes) : Bytes#

What a certificate-mode node signs to prove it holds its certificate's key: the PEER's nonce (the challenge) and the transcript, which covers both hellos (identities, roles, addresses, certificates and the ephemeral X25519 keys), so a man in the middle cannot swap any of them.

fncert_provecert_prove(secret_key : Bytes, challenge : String, th : Bytes) : Bytes#

Certificate-mode proof: the ed25519 signature, under [secret_key], of cert_proof_message(challenge, th), where [challenge] is the PEER's nonce and [th] the handshake transcript.

fncert_verifycert_verify(pubkey : Bytes, challenge : String, th : Bytes, proof : Bytes) : Bool#

Check a peer's certificate-mode [proof]: true when it is a valid ed25519 signature by [pubkey] (the key in the peer's verified certificate) over cert_proof_message(challenge, th), with [challenge] the nonce WE sent.

fnframe_keysframe_keys(ikm : Bytes, th : Bytes, my_nonce : String, peer_nonce : String) : FrameKeys#

The per-connection MAC keys from input key material [ikm] (the X25519 shared secret, or the cluster secret) and the transcript [th]: one key per direction, told apart by the SENDER's nonce, so a frame reflected back to its sender does not verify.

fnhkdf_expandhkdf_expand(prk : Bytes, info : String) : Bytes#

HKDF-SHA256 expand to one 32-byte block for [info].

fnhkdf_extracthkdf_extract(salt : Bytes, ikm : Bytes) : Bytes do hmac_sha256_bytes(salt, ikm) end#

HKDF-SHA256 extract (RFC 5869): the pseudorandom key for [ikm] under [salt].

fnmac_suffixmac_suffix() : String do "~mac1" end#

The nonce suffix, "~mac1", by which a shared-secret node offers to MAC every frame after the handshake.

fnmode_namemode_name(a : Auth) : String#

The handshake mode name for [a]: "shared-secret" for Secret(_), "certificate" for Certified(_). Used in logs and mode-mismatch errors.

fnoffers_macoffers_mac(nonce : String) : Bool do String.ends_with(nonce, mac_suffix()) end#

True when [nonce] ends with mac_suffix(), i.e. its sender offers to seal frames with a MAC after the handshake. The MAC is used only when both sides' nonces carry the offer.

fnproveprove(secret : String, nonce : String) : String#

Shared-secret proof that the prover holds the cluster secret, bound to the challenge [nonce]: HMAC-SHA256(secret, nonce) as a lowercase hex string. The secret itself never goes on the wire. Deterministic: the same secret and nonce always give the same proof.

fntranscripttranscript(my_hello : List(Int), my_nonce : String, peer_hello : List(Int), peer_nonce : String) : Bytes#

The handshake transcript hash: HMAC-SHA256 under a fixed label of both hellos' exact bytes, the one with the smaller nonce first, so both sides compute the same value. (Keyed by a public label it is simply SHA-256's collision resistance; hmac_sha256_bytes is the digest builtin that returns raw Bytes on both backends.)

fnverifyverify(secret : String, nonce : String, proof : String) : Bool#

Verify a peer's [proof] for a [nonce] we issued: true exactly when [proof] equals prove(secret, nonce). False means the peer lacks the secret or replayed a proof for a different nonce. The comparison is plain string equality (not constant-time).