nx_mls_group.nx
buildroot/runtime/nx_mls_group.nx
about
nx_mls_group.nx -- C7 of the comms lane: MLS-CLASS EPOCHED GROUP RATCHET (RFC 9420 key schedule).
Contract symbol mls_epoch_advance == the /compare/comms C7 watch. REFEREE: nx_mls_group_gate.
DONE-RULE (comms.plan): RFC 9420-class epoched group keys on chat AND calls; member removal forces
an epoch advance and the gate PROVES a removed member cannot decrypt the next frame (negative
control), with forward-secrecy and post-compromise teeth; replaces the static room key.
HONEST SCOPE, stated so nothing is over-claimed: this implements the RFC 9420 KEY SCHEDULE and its
security properties -- one-way epoch chaining (forward secrecy), commit-secret injection
(post-compromise security), and membership-bound epoch secrets (removal excludes). Commit secrets
are distributed to the remaining members by FLAT per-member X25519 (O(n) wraps). TreeKEM's ratchet
tree is the O(log n) DISTRIBUTION efficiency, and it is a follow-on, NOT a security gap -- every
property proven by the gate is the same property TreeKEM delivers, just distributed pairwise here.
PURE COMPOSITION of the sovereign primitives, no new crypto: x25519 (RFC 7748 ECDH) + sha256
(FIPS 180-4, as HMAC/HKDF-Expand) + chacha20 (RFC 8439). No WebRTC, no OpenSSL, one auditable stack.
THE PROPERTIES, and how each is a tooth:
* epoch_secret is per-member, never transmitted; only COMMITMENTS sha256("commit-check"||secret)
are printed, so the gate can prove convergence without exposing a secret.
* advance: prk = HMAC(old_epoch_secret, commit_secret); new = HKDF-Expand(prk,"epoch",group_ctx).
One-way (HKDF) => forward secrecy. commit_secret fresh + ECDH-wrapped => post-compromise.
* group_ctx = sha256(group_id || epoch || roster) => the epoch secret is BOUND to the membership,
so a divergent roster diverges the secret (defence in depth behind the wrap exclusion).
* a removed member is absent from the new roster, so no wrap is addressed to it: apply REFUSES,
and even holding the old epoch_secret it cannot reach the new one (no commit_secret).
* frames are real ChaCha20 sealed under framekey=HKDF(epoch_secret,"app",seq) + an HMAC tag, so
open with the WRONG epoch_secret fails the MAC -- the removed member cannot open the next frame.
license_tier: ORIGINAL No hw writes (Rule 26).
dependencies 5 imports · 0 importers
imports: nx_syscalls.nxnx_itoa_lib.nxnx_x25519.nxnx_sha256.nxnx_chacha20.nx
imported by: nobody (leaf or entry point)
call flow from main pre-order; caps 40 nodes / depth 6 declared; ↻ = already shown
structs
| none |
consts
| 34 | const MG_EXIT_OK: i64 = 0 |
| 35 | const MG_EXIT_USAGE: i64 = 2 |
| 36 | const MG_EXIT_REFUSED: i64 = 3 |
| 37 | const MG_EXIT_CORRUPT: i64 = 5 |
| 39 | const MG_KEY: i64 = 32 // X25519 scalar / pubkey / epoch_secret / chacha key |
| 40 | const MG_MAC: i64 = 32 // HMAC-SHA256 tag |
| 41 | const MG_NONCE: i64 = 12 // chacha20 nonce |
| 42 | const MG_BLOCK: i64 = 64 // sha256/HMAC block |
| 43 | const MG_HEXCAP: i64 = 4096 |
| 44 | const MG_FILECAP: i64 = 262144 |
| 45 | const MG_NL: i64 = 10 |
| 46 | const MG_PIPE: i64 = 124 |
| 47 | const MG_COMMA: i64 = 44 |
| 48 | const MG_IPAD: i64 = 54 // 0x36 |
| 49 | const MG_OPAD: i64 = 92 // 0x5c |
| 50 | const MG_ROSTER_MAX: i64 = 256 |
functions
| 52 | func mg_w(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } sys_write(1, s, n); return 0 } |
| 53 | func mg_wn(s: *u8, n: i64) -> i64 { sys_write(1, s, n); return 0 } |
| 54 | func mg_num(v: i64) -> i64 { nxi_out(v); return 0 } |
| 55 | func mg_len(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } |
| 56 | func mg_eq(a: *u8, b: *u8) -> i64 called by 1: main |
| 62 | func mg_cpy(d: *u8, s: *u8, n: i64) -> i64 { var i: i64 = 0; while i < n { d[i] = s[i]; i = i + 1 } return 0 } |
| 63 | func mg_memeq(a: *u8, b: *u8, n: i64) -> i64 { var i: i64 = 0; while i < n { if a[i] != b[i] { return 0 } i = i + 1 } return 1 } |
| 65 | func mg_hexe(src: *u8, n: i64, dst: *u8) -> i64 |
| 78 | func mg_hexv(c: i64) -> i64 called by 1: mg_hexd |
| 84 | func mg_hexd(src: *u8, n: i64, dst: *u8) -> i64 |
| 98 | func mg_read(path: *u8, buf: *u8, cap: i64) -> i64 called by 7: mg_pubverbmg_joinmg_commitmg_applymg_epochverbmg_seal+1 calls 3: sys_openat_rdsys_readsys_close |
| 106 | func mg_write(path: *u8, buf: *u8, n: i64) -> i64 |
| 115 | func mg_cat(d: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { d[o] = s[i]; o = o + 1; i = i + 1 } return o } |
| 116 | func mg_catn(d: *u8, o: i64, v: i64) -> i64 |
| 127 | func mg_hmac(key: *u8, klen: i64, msg: *u8, mlen: i64, out: *u8) -> i64 called by 6: mg_derivemls_epoch_advancemg_wrapmg_unwrapmg_sealmg_open calls 2: sys_mmapsha256_digest |
| 155 | func mg_derive(secret: *u8, label: *u8, ctx: *u8, clen: i64, out: *u8) -> i64 |
| 167 | func mg_ctx(group_id: *u8, epoch: i64, roster: *u8, out: *u8) -> i64 |
| 182 | func mls_epoch_advance(old_secret: *u8, commit_secret: *u8, group_id: *u8, epoch: i64, roster: *u8, out: *u8) -> i64 |
| 191 | func mg_clamp(p: *u8) -> i64 |
| 196 | func mg_pub(priv: *u8, pub: *u8) -> i64 |
| 205 | func mg_wrap(eph_priv: *u8, member_pub: *u8, payload: *u8, out: *u8) -> i64 |
| 218 | func mg_unwrap(member_priv: *u8, eph_pub: *u8, blob: *u8, payload: *u8) -> i64 |
| 232 | func mg_commit_hash(secret: *u8, out: *u8) -> i64 |
| 241 | func mg_roster_idx(roster: *u8, mypubhex: *u8) -> i64 |
| 262 | func mg_field(line: *u8, n: i64, idx: i64, out: *u8) -> i64 |
| 276 | func mg_find_wrap(buf: *u8, n: i64, idx: i64, blob: *u8) -> i64 |
| 308 | func mg_keygen(priv_out: *u8, seed_hex: *u8) -> i64 |
| 329 | func mg_pubverb(priv_file: *u8) -> i64 |
| 345 | func mg_create(group_id: *u8, seed_hex: *u8, eph_hex: *u8, welcome_out: *u8, roster: *u8) -> i64 |
| 407 | func mg_join(welcome_file: *u8, priv_file: *u8, state_out: *u8) -> i64 |
| 461 | func mg_state_line(buf: *u8, n: i64, which: i64, out: *u8) -> i64 |
| 476 | func mg_commit(state_file: *u8, eph_hex: *u8, new_roster: *u8, commit_out: *u8, new_state_out: *u8) -> i64 |
| 579 | func mg_apply(commit_file: *u8, priv_file: *u8, old_state_file: *u8, new_state_out: *u8) -> i64 |
| 655 | func mg_epochverb(state_file: *u8) -> i64 |
| 677 | func mg_framekey(es: *u8, seq: i64, fk: *u8, nonce: *u8) -> i64 |
| 688 | func mg_seal(state_file: *u8, seq: i64, plaintext: *u8, frame_out: *u8) -> i64 |
| 735 | func mg_open(state_file: *u8, frame_file: *u8) -> i64 |
| 780 | func mg_usage() -> i64 |
| 784 | func main(argc: i64, argv: *i64) -> i64 |