Files
BackOne/SPEC.md
T
dedysutanto fa08792c78
build.yml / build_ubuntu (push) Canceled after 0s
build.yml / build_macos (push) Canceled after 0s
build.yml / build_windows (push) Canceled after 0s
Lint / clang-format (push) Canceled after 0s
validate.yml / build_ubuntu (push) Canceled after 0s
feat(pqc): plumb pqcMode to identity path, hybrid HELLO ct, spec review fixes
- Node stores _pqcMode; Identity::generate(bool,int) derives pq from daemon mode
- OneService reads local.conf settings.pqcMode before new Node (opt-in, absent=classic)
- Peer/IncomingPacket: hybrid KEM ct in HELLO/OK, capability-bit fallback classic
- tools/pqc-lab.sh two-node E2E lab + tools/zt-dissector.lua capture dissector
- docs/pqc-test-scenario.md test plan
- SPEC.md review pass: V12 evidence -> live daemon check, V14/V15 cite realign,
  T24/T25 tasks (pqconly preset gap documented)
2026-10-03 07:01:52 +07:00

108 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SPEC — BackOne
## §G (goal)
Make BackOne quantum ready: phases 1-6 of `backone-quantum-fork-research.md` — liboqs + hybrid X25519+ML-KEM-768 / Ed25519+ML-DSA-65 handshake, identity/COM v2 double-sign, capability-bit negotiation. Phase 7 (AES-256-GCM) OUT.
## §C (constraints)
- C++17. GNU make = real build. Top `CMakeLists.txt` = stub, never product build.
- Deps vendored in `ext/`. No new dep for few-liners.
- Tabs always, `.clang-format` LLVM/Stroustrup, indent 4. No reformat of untouched lines.
- New `.cpp/.hpp/.h` gets 11-line BSL header verbatim (`node/Mutex.hpp:1-12`).
- `node/` stays OS-independent. Sockets/files → `osdep/`. Orchestration → `service/`. Persistence → `controller/`.
- No exceptions across C API. Fatal `ZT_ResultCode` = 100–999.
- One reactor thread (`Phy::poll`). Cross-thread wake only `Phy::whack()`.
- New compiled file → register in `objects.mk` or no link.
- Platforms: Linux (primary), macOS, BSD/NetBSD, Windows (msbuild).
- Header edits: no `-MMD` dep tracking → `make clean` after touching `.h`.
- PQC lock: liboqs static vendored `ext/`; hybrid key = KDF(classical ‖ pq); presets `hybrid` (ML-KEM-768/ML-DSA-65) + `pqconly` (intended ML-KEM-1024/ML-DSA-87 — not yet distinct, see T25)
- wire/identity/COM use versioned v2 + capability-bit fallback to classic
- `include/ZeroTierOne.h` changes additive-only (Java/libzt ripple)
- OUT: AES-256-GCM swap, trusted-path skipCrypto, forward secrecy, protocol standardization, crypto audit, production release/signing
- `?` address v2 derivation keeping 40-bit + PoW
- `?` VL1 fragmentation capacity for +4KB identity (check `node/Packet.*`)
- `?` liboqs version/tag + build flags for `ext/`
- `?` measured ML-KEM/ML-DSA latency (doc estimates unverified)
- `?` capability-bit placement in existing handshake
## §I (public surfaces)
| id | surface | detail |
|---|---|---|
| I.capi | C API | `include/ZeroTierOne.h`, `ZT_Node_*` (lines 1796-2258), `ZT_SDK_API`. Ripples to Java/libzt/SDK |
| I.api | HTTP JSON API | `127.0.0.1:9993`, `/status /peer /network /member /controller /metrics /info`, auth `X-ZT1-Auth` header or `?auth=` from `authtoken.secret` |
| I.cli | CLI | `backone-cli info\|listpeers\|listnetworks\|join\|leave` (`-j -p -D`), `backone-idtool generate\|validate\|getpublic\|sign\|verify\|mkcom` — symlink dispatch on `argv[0]` |
| I.cfg | config | home `/var/lib/backone` (macOS `/Library/Application Support/BackOne`), `local.conf`, `identity.secret`, `networks.d/`, `controller.db`, `backone.port`, `backone.pid` |
| I.build | build | `make`, `make one`, `make selftest`, `make core`, `make debug`, `make install`, `make debian/redhat`, `make central-controller`; env `ZT_*` knobs |
| I.ffi | Rust FFI | `rustybits/zeroidc` (`zeroidc_new`…), `smeeclient` — C header `zeroidc.h`, link on `ZT_SSO_SUPPORTED=1`/`ZT_CONTROLLER=1` |
| I.jni | Java SDK | `java/jni/…Node.cpp`, ant targets `build_java/build_android/build_jar` |
| I.pqc.cfg | config | `local.conf` `settings.pqcMode` (off/hybrid/pqconly); **absent = off/classic (opt-in)**, unknown value = warn + classic. Per-algorithm `kem`/`sig` selection is *not* wired (research doc only, see T25) |
| I.pqc.wire | wire | static-static hybrid, no ephemeral exchange: identity v2 (type byte 1) carries ML-KEM-768 pub + ML-DSA-65 pub; agree = KDF(X25519-identity-agree ‖ ML-KEM-encaps(peer static pub)) (`node/Identity.hpp:364-379` encaps / `:392+` decaps, driven from `node/Peer.cpp` `ensurePendingHybridCt`/`setHybridSessionKey`); capability via HELLO protocol-version bytes (`node/Packet.hpp:329-333`); `identity.secret`/`identity.public` v2 on disk |
| I.pqc.com | credentials | COM double-sign/verify — controller + `node/CertificateOfMembership.*` |
| I.rules | rules compiler | `node rule-compiler/cli.js <rules>` → rules/tags JSON |
## §V (invariants)
Derived from `selftest.cpp` + CI gates. `?` = code-derived, no test yet.
| id | invariant | evidence |
|---|---|---|
| V1 | Crypto KATs pass: Salsa20 vectors, C25519 agreement, Ed25519 sign/verify, SHA512, Poly1305 | `selftest.cpp:137` `testCrypto` |
| V2 | Identity known-good accepted, known-bad rejected | `selftest.cpp:475` `testIdentity` |
| V3 | CertificateOfMembership signs and `agreesWith` roundtrip | `selftest.cpp:577` `testCertificate` |
| V4 | Packet encode/decode roundtrip exact | `selftest.cpp:641` `testPacket` |
| V5 | Phy UDP+TCP loopback on 127.0.0.1 works, no root | `selftest.cpp:1002` `testPhy` |
| V6 | Zero definite leaks under valgrind; test exit code 0 | `.github/workflows/validate-report.sh` |
| V7 | `?` No exception crosses C API; `ZT_ResultCode_isFatal` only 100–999 | `include/ZeroTierOne.h:426` |
| V8 | `?` `node/` contains no OS calls (sockets/files/interfaces) | `node/README.md` rule |
| V9 | hybrid↔vanilla negotiates classic, link stays up | interop matrix |
| V10 | identity v2 survives 1280B MTU tunnel intact; v2 = type byte 1 (wire `Identity.hpp:219`, string slot `Identity.cpp:173`); address derivation hashes X25519 material only → v1/v2 addresses bit-identical; v1 parser rejects type ≠ 0 cleanly | interop matrix |
| V11 | ML-KEM/ML-DSA KATs + hybrid KDF vector + identity v2 roundtrip + COM double-sign verify pass | extended `selftest.cpp` |
| V12 | `pqcMode` negotiation honors `local.conf` (`off` -> no v2 fields) | live daemon: bogus `settings.pqcMode` -> `WARNING` (`service/OneService.cpp:1478`), `hybrid` -> clean (2026-10-03); `selftest` has no `pqcMode` coverage |
| V13 | hybrid agree = KDF(X25519-identity-agree ‖ ML-KEM static-static), no ephemeral exchange; hybrid↔hybrid agree + hybrid↔vanilla classic fallback pass | `node/Peer.cpp:105-117` (encaps) `node/IncomingPacket.cpp:534-542,697-703` (decaps) |
| V14 | COM double-sign = type byte 1→2 appends ML-DSA sig alongside Ed25519; v1 verify path unchanged; v1 rejects type 2 cleanly | `node/CertificateOfMembership.hpp:222-296`, `node/CertificateOfMembership.cpp:97-162`, `selftest.cpp:717` |
| V15 | handshake vs fragment loss measured: PQC identity in clear HELLO = 3273B → 3 frags (classic 137B → 1 frag, max 7 `Packet.hpp:235`); Monte Carlo 20k trials obs≈analytic at p∈{.01,.05,.10,.30}; fragment loss delays HELLO (retransmit), does not break handshake | `selftest.cpp:1713` benchmark |
| V16 | hybrid handshake puts a 1088 B ML-KEM-768 ct on the wire and both sides derive the same hybrid `_key`; classic/off peers carry none and fall back | write `node/Peer.cpp:493-502`, parse `node/IncomingPacket.cpp:534-542`, OK echo `node/IncomingPacket.cpp:608-619,697-703`; E2E lab M1-M5 all pass `tools/pqc-lab.sh` (2026-10-02; M3 68 / M4 90 large PQ datagrams on wire) |
## §T (tasks)
| id | status | task | cites |
|---|---|---|---|
| T1 | x | Fix CI build smoke: `build.yml` runs `./zerotier-selftest`, build emits `backone-selftest` (also `tar zerotier-one` → `backone`) | I.build,V6 |
| T2 | x | Fix `validate-linux.sh` binary names: invokes `./zerotier-one`, `./zerotier-cli` (lines 41-43,105,111,451), builds `backone*` | I.cli,V6 |
| T3 | x | Wire `ZT_COVERAGE` into makefiles — consumed by none, gcovr likely reports zeros `?` | I.build |
| T4 | x | `make install` manpage rule: expects `doc/backone.8`, `doc/backone-cli.1`, `doc/backone-idtool.1`; `doc/` only has `zerotier-*`, no rename rule | I.build |
| T5 | x | `make redhat`: `backone.spec` %install copies missing `debian/backone.service` + `ext/installfiles/linux/backone.init.rhel6` (actual files `zerotier-*`) | I.build |
| T6 | x | Rebrand `make-bsd.mk`/`make-netbsd.mk`: still emit/install `zerotier-one`, `/var/db/zerotier-one` vs code writing `backone.pid` | I.build |
| T7 | x | `Dockerfile.ci`: copies `zerotier-one` from build → fails; binary is `backone` | I.build |
| T8 | . | `pkg/snap/snapcraft.yaml`: builds upstream `github.com/zerotier/zerotierone.git`, expects `usr/sbin/zerotier-one` | I.build |
| T9 | . | Wire orphaned `java/test/StringUtilsTest.java` (JUnit4) into `java/build.xml` target or delete | I.jni |
| T10 | . | Replace `rule-compiler/package.json` `npm test` failing placeholder with real test of `rule-compiler.js` | I.rules |
| T11 | . | `?` Add `#[test]` coverage for `rustybits/zeroidc` FFI parse/exchange paths | I.ffi,V7 |
| T12 | x | Implement Redis config TODO (`service/OneService.cpp:1467` `// TODO: Redis config`) | I.cfg |
| T13 | . | Implement `LFDB::eraseNetwork`/`eraseMember` TODOs (`controller/LFDB.cpp:383,388`) | I.cfg |
| T14 | . | ~~Lint gate~~ done: `make lint` (`tools/lint.sh`, changed-lines clang-format) + `.github/workflows/lint.yml` | I.build |
| T15 | . | Fix `-Wsign-compare` suppression FIXME (`node/Bond.cpp:23`, `node/Node.cpp:40`) | I.build |
| T16 | x | Federation identity-collision payload FIXME (`node/IncomingPacket.cpp:211`) `?` scope | I.capi,V7 |
| T17 | x | Phase 1: vendor liboqs static into `ext/`, wire `make-linux.mk` + `objects.mk`, pin version/tag + flags | I.build |
| T18 | x | Phase 2: `node/PQHybrid.*` hybrid agree = KDF(X25519-identity-agree ‖ ML-KEM-768 encaps over peer static pub); phase-2 ephemeral-field mockup dropped | I.pqc.wire,V13 |
| T19 | x | Phase 3: identity v2 type byte 1, append ML-KEM+ML-DSA pubs, address hash X25519-only, v1 clean reject type ≠ 0 | I.pqc.wire,V10 |
| T20 | x | Phase 4: COM type byte 2 double-sign (Ed25519+ML-DSA-65), v1 rejects cleanly | I.pqc.com,V14 |
| T21 | . | Phase 5: `local.conf` `settings.pqcMode` parse (off/hybrid/pqconly) + capability negotiation via HELLO protocol-version bytes, fallback classic. Mode now selects *whether* PQ material is used; *which* preset is still hardcoded (T24) | I.pqc.cfg,V12,V9 |
| T22 | x | Phase 6: selftest extension — ML-KEM/ML-DSA KATs, hybrid KDF vector, identity v2 roundtrip + address stability, COM double-sign, interop hybrid↔vanilla matrix | I.build,V11,V10,V14 |
| T23 | x | Phase 6: handshake-vs-fragment-loss benchmark for PQC HELLO; cap/segment if loss breaks handshake | V15 |
| T24 | x | Plumb `settings.pqcMode` through the identity path: `Node(…,int pqcMode)` stores `_pqcMode`; `Identity::generate(bool pq,int pqcMode)` derives `pq` from the daemon mode; `OneService` reads `local.conf` before `new Node` — a fresh hybrid/pqconly node writes a type-1 identity, a classic one type-0. Fixes the G1 defect. | I.pqc.cfg,V12 |
| T25 | . | Distinct PQ parameter sets: `node/PQHybrid.*` hardcodes ML-KEM-768/ML-DSA-65 (`PQHybrid.cpp` static_asserts `OQS_KEM_ml_kem_768`/`OQS_SIG_ml_dsa_65`), so `hybrid` and `pqconly` currently select identical material, while `SPEC` §C claims ML-KEM-1024/ML-DSA-87 for `pqconly`. Parameterize the preset (key sizes, ct len, `_pendingHybridCt` buffer, HELLO guard) or drop the `pqconly` claim. Note a `pqconly` HELLO with a PQ identity is 3273B → 3 frags, well under the 10024B cap, so no clean-fail path is reachable today | I.pqc.wire,I.pqc.cfg,V15 |
T1/T2 = prerequisites — CI must invoke `backone-selftest` before PQC gates mean anything. T1–T16 backlog (rebrand/CI/lint leftovers); T17–T25 = PQC phases 1–6 + the pqconly preset gap.
Not tracked (low value `?`): `one.cpp:256` port/token cleanup, `MacKextEthernetTap.cpp:480` iface status, VLAN TODOs in `NetBSDEthernetTap.cpp:468`/`MacKextEthernetTap.cpp:685`, `Phy.hpp:325` unix sock type comment.
## §B (bug log)
| id | date | cause | fix |
|---|---|---|---|
| B1 | 2026-10-02 | Phase 5 (T21) advertised PQC capability in HELLO but never consumed it: `Peer::_key` was derived classically in the constructor and the hybrid ciphertext had no place in the HELLO/OK(HELLO) layout, so `hybridEligible()` was dead. | Wire hybrid static-static KEM ct into HELLO (lowest-address side writes `[moon count][moons][flag(1B)][ct 1088B]`, crypted with the classical key) and echo it in OK(HELLO); parser order matches `Peer::sendHELLO`/`_doHELLO`; `Peer::ensurePendingHybridCt()` lazily encapsulates on first capability sighting so the handshake completes in one round trip; direction guard `sender == lowest address` on both parses. Refuse to start when the on-disk identity cannot satisfy `settings.pqcMode` (classic↔PQ migration changes the address). |
| B2 | 2026-10-02 | `settings.pqcMode` absent (or unrecognized) resolved to HYBRID, and `Node`'s `pqcMode` default was HYBRID with no way to tell "unconfigured" from "configured hybrid". Every existing type-0 install would hit the B1 refusal guard at startup; the C API `ZT_Node_new` path would silently generate type-1 identities. | Absent/unknown `settings.pqcMode` now resolves to `ZT_PQC_MODE_CLASSIC` (PQC is opt-in, matching the research doc); `Node(…,int pqcMode = ZT_PQC_MODE_CLASSIC)`. Verified: classic identity + no config boots on 2.0.0 and stays 141 B; fresh node w/o config makes type-0; fresh node with `"hybrid"` makes type-1 (6415 B); classic + explicit `"off"` boots; classic + explicit `"hybrid"` still refuses; unknown value warns and runs classic. |