Files
BackOne/SPEC.md
T
dedysutanto fa08792c78 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

13 KiB
Raw Blame History

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.