Commit Graph

8 Commits

Author SHA1 Message Date
bb39e76326 feat(cli): peer ping — verify mutual PQ-mTLS connectivity to a peer
First consumer of the client side: buh_api::peer::probe_peer opens a PQ-mTLS
connection (presenting this node's leaf, pinning the peer's CA from the trust
registry), fetches /v1/health, and reports the peer's advertised CA fingerprint.
A probe succeeds only when BOTH nodes trust each other's CA, so it is a true
bidirectional trust check.

Deliberately not node↔node envelope forwarding — that belongs to the deferred
§10 mailbox-redundancy work and would be surface with no consumer today.

buh-cli gains `peer ping <host:port>`; it links buh-api (the node's own admin
tool). Documented caveat: Turso locks the datastore exclusively, so CLI commands
cannot run while this host's daemon holds the DB (a daemon-side admin path is the
fix — tracked for the next session).

cargo check/fmt/clippy(--all-features) clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EB3LjarCdXxqrJ4tFLn8LB
2026-06-30 16:02:55 +03:00
9a9cf0d609 feat(tls): decentralised PQ-mTLS ingress + per-node CA trust model (Phase 6)
Every node is its own CA — no central PKI, no step-ca. Peers are pinned per-CA
by fingerprint (not a shared root); a peer is refused the instant its CA is
distrusted. The TLS key exchange is the X25519MLKEM768 hybrid, so a recorded
handshake is not harvest-now-decrypt-later material.

- buh-core: NodePki/NodeLeaf/PeerTrustRegistry/TrustedPeer ports (DER bytes +
  fingerprint strings only — core stays free of rustls/rcgen); Ctx.pki +
  Ctx.peer_trust.
- buh-data: RcgenNodeCa (ECDSA-P256 CA persisted under the pki dir; fingerprint
  stable across restart; load_or_init/rekey/issue_leaf) and TursoPeerTrust
  (migration 0002_peer_trust.sql). DataStack::with_node_pki.
- buh-api/src/tls.rs: pq_provider() (X25519MLKEM768 first), custom
  PinnedClient/ServerCertVerifier (sync, pin CA-by-fingerprint at the chain tail
  + verify leaf↔CA signature + validity via x509-parser), RotatingResolver,
  NodeTls. main.rs serves plain on `bind` OR PQ-mTLS on [pki].node_bind
  (BUH_NODE_PORT) via tokio-rustls + hyper-util, with an in-process
  leaf-rotation/trust-refresh timer. /v1/health advertises ca_fingerprint.
- buh-cli: ca init|show|rotate --force, peer trust|distrust|list.
- web: invite carries the node's real CA fingerprint (from /v1/health, zero in
  plain dev); relay.pinCa/verifyPinnedCa make the app-layer pin explicit; demo
  + UI surface it.
- test: hermetic two-node handshake succeeds only with mutual per-CA pinning AND
  X25519MLKEM768; refused without a pin; refused after distrust.

The CA signatures are classical (ECDSA-P256) by design: HNDL threatens
confidentiality (the key exchange, which is PQ), not signatures that only need
to hold at handshake time.

The node never links buh-crypto; buh-crypto links none of the TLS deps.
cargo test/clippy(--all-features)/fmt + wasm-pack + web build all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EB3LjarCdXxqrJ4tFLn8LB
2026-06-30 15:47:06 +03:00
479260350d feat(blob): blob role + per-file content-key media path (Phase 5)
Large media never crosses the relay envelope. The sender seals a file once
under a fresh single-use content key, uploads the opaque ciphertext to a blob
node that cannot read it, and folds only {content key + locator} into the
ratchet. This lands that path end to end.

- buh-crypto `media`: seal_media/open_media over XChaCha20-Poly1305 with a
  per-file content key, domain-separated from envelope sealing; MediaKey
  (key‖nonce) serializes for folding into an envelope. Exposed through the WASM
  facade (seal_media → SealedMedia{key_material, ciphertext}; open_media) and
  re-exported in the web crypto index.
- buh-core: BlobStore is now a live port. `Ctx.blob` is Some only on nodes
  running the blob role; `blob` orchestration gates on the role (501 otherwise),
  clamps size (max_blob_bytes), and stays as thin and blind as the relay.
- buh-data: FsBlobStore (filesystem/ZFS, path-traversal-safe, atomic writes) is
  always built; S3BlobStore (S3/MinIO) sits behind an opt-in `s3` feature so
  filesystem nodes don't pull the AWS SDK. DataStack gains with_fs_blob /
  with_s3_blob builders.
- buh-api: PUT/GET /v1/blob/{bucket}/{*key} serve opaque ciphertext, body-limit
  bounded to max_blob_bytes; a [blob] config section opts the node into the role
  and picks the backend. Integration test proves byte-verbatim round-trip,
  404/501/400 edges, and that a non-blob node refuses.

Fix the web `wasm` npm script to an absolute --out-dir: wasm-pack resolves it
relative to the crate, so the old `../web/...` silently wrote the pkg to a stray
crates/web/ instead of the gitignored web/src/lib/crypto/pkg/.

cargo test/clippy(--all-features)/fmt, wasm-pack test --node, and the web build
all green. 57 buh-crypto native tests; 5 blob HTTP tests; Fs + core blob tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMbqdSzDP4F7Y9AdK5LXaV
2026-06-30 14:43:22 +03:00
8e6e734c14 feat(crypto): Double Ratchet over the PQXDH root (Phase 3 complete)
Per-message forward secrecy from HKDF-SHA256 symmetric chains plus
post-compromise healing from an X25519 DH step each time the conversation turns
(design.md §5.3). Hash chains are already quantum-safe, so the future PQ-rekey
layer slots in at the root KDF via the reserved wire tags — additive, no wire
break.

- chain.rs: KDF_RK / KDF_CK / message-key expansion, all HKDF-SHA256.
- header.rs: ratchet header (DH pub, PN, N) as canonical AEAD additional data, so
  header tampering fails decryption.
- mod.rs: RatchetState with initiator/responder init handing off from pqxdh (the
  responder's signed prekey is its first ratchet key), DH-ratchet steps, and a
  bounded skipped-key store (MAX_SKIP per chain, hard cap across chains) for
  out-of-order / dropped delivery.

Tests: full handshake → bidirectional chat over many DH steps; out-of-order,
dropped, replayed, and tampered messages handled correctly; proptest interop
over permuted+dropped delivery and long bidirectional runs (native only —
proptest's wait-timeout dep won't build for wasm). The whole client path
(hybrid PQXDH + ratchet) also runs green under wasm-bindgen-test in Node, the
same code the browser loads.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMbqdSzDP4F7Y9AdK5LXaV
2026-06-30 13:17:46 +03:00
11d1bb97a6 feat(crypto): hybrid X25519 + ML-KEM-768 KEM primitives (Phase 3)
The two halves of the harvest-now-decrypt-later defence (design.md §5.2),
each a standalone primitive; pqxdh combines them into a root key next.

- X25519 (x25519-dalek): secret/public keys, encode round-trip, diffie_hellman.
  Secrets seeded from getrandom directly (no rng-trait plumbing, wasm-clean).
- ML-KEM-768 (ml-kem 0.3.2, FIPS 203): keygen, encapsulate/decapsulate, key
  encode/parse via kem's KeyExport; system-RNG methods route through getrandom
  0.4 (wasm_js in browser).

Tests: X25519 DH agreement + encoding round-trip; ML-KEM encapsulate↔decapsulate
agreement, public-key round-trip, and the implicit-rejection property (a wrong
decapsulation key yields a different, non-erroring secret — so transcript
binding, not a decap error, is what authenticates). Compiles to wasm32.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMbqdSzDP4F7Y9AdK5LXaV
2026-06-30 13:05:42 +03:00
2d1c5a06a6 feat(web): Vite/WASM boundary proof + browser-run crypto KATs (Phase 0 tail)
Closes the Phase-0 scaffold: buh-crypto now compiles to WebAssembly and the web
client exercises the whole crypto stack in the browser before any session code
depends on it.

buh-crypto:
- `wasm` feature gates a small wasm-bindgen FFI (src/ffi.rs): echo plus
  wire/aead/identity self-tests. `#![forbid(unsafe_code)]` is kept for the
  native core and relaxed only under `wasm` (bindgen's glue is the only unsafe).
- getrandom standardised on 0.4 (the version crypto-common under ml-dsa pulls)
  with the `wasm_js` backend selected via .cargo/config.toml for
  wasm32-unknown-unknown; chacha20poly1305 default features off to drop the old
  getrandom-0.2 path.
- tests/wasm.rs runs the wire / XChaCha20-Poly1305 / ML-DSA-65 KATs under
  wasm-bindgen-test — the deterministic-signature digest matches native
  byte-for-byte, so every primitive is now gated native AND wasm.

web/: Vite + React + SWC + TS app wired with vite-plugin-wasm +
vite-plugin-top-level-await over a `--target bundler` wasm-pack build. On load
it round-trips a Uint8Array through echo and runs the three KATs in-browser;
verified in Firefox — all four checks green, zero console errors. All crypto is
imported through src/lib/crypto/, never from the generated pkg/ directly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMbqdSzDP4F7Y9AdK5LXaV
2026-06-30 12:59:28 +03:00
d175634001 feat(crypto): ML-DSA-65 identity + XChaCha20-Poly1305 AEAD (Phase 2)
The user IS their key (design.md §4): identity is an ML-DSA-65 (FIPS 204)
keypair, hedged/randomized signing by default with a deterministic variant for
reproducible vectors. Fixed-size encode/parse for the public key (1952B) and
signature (3309B), verify-on-failure that leaks only that it failed.

AEAD: nonce-explicit XChaCha20-Poly1305 sealing primitive — the caller owns the
nonce (the ratchet will), so seal() is pure and KAT-pinnable; random_nonce() for
the one-off media-key cases. AAD is the place to bind the wire prelude+flags.

Tests gate the primitives:
- AEAD: the published draft-arciszewski-xchacha-03 A.1 vector (ciphertext+tag
  byte-exact), plus tamper/ciphertext/tag/AAD-downgrade/wrong-key rejection.
- Identity: sign/verify round-trip, seed determinism, encode round-trip + length
  checks, and a regression KAT pinning the seed=0 public key and deterministic
  signature by SHA-256 digest (catches encoding / wasm divergence; upstream
  ml-dsa carries the NIST ACVP vectors).

Adds ml-dsa (getrandom feature → hedged signing), chacha20poly1305, getrandom
0.3, sha2, hkdf to the workspace; only buh-crypto links them — the node never
does. CryptoError unifies wire/aead/signature/parse failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UMbqdSzDP4F7Y9AdK5LXaV
2026-06-30 12:50:45 +03:00
5a19fc1ab2 feat: scaffold buh cargo workspace and pure layers
Establish the AGPL-3.0 cargo workspace per the org conventions, with the
crate layout for the anti-hub messenger: buh-entities (domain types/DTOs),
buh-crypto (client crypto core; compiles to WASM, never linked by the node),
and buh-core (business logic + MailboxRepo/BlobStore/SettlementBackend ports).

The settlement value types are deliberately chain- and money-free (design
§8.5); buh-core stays honestly thin, as a blind relay has little logic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 12:29:02 +03:00