phantom-protocol 0.1.1

Post-quantum-secure L4/L6 universal transport framework — hybrid X25519+ML-KEM-768 / Ed25519+ML-DSA-65, multi-path, UniFFI bindings
Documentation
[package]
name = "phantom-protocol"
version = "0.1.1"
edition = "2021"
rust-version = "1.75"
description = "Post-quantum-secure L4/L6 universal transport framework — hybrid X25519+ML-KEM-768 / Ed25519+ML-DSA-65, multi-path, UniFFI bindings"
license = "Apache-2.0"
repository = "https://github.com/snaart/phantom_protocol"
documentation = "https://docs.rs/phantom-protocol"
readme = "README.md"
keywords = ["post-quantum", "transport", "cryptography", "networking", "ml-kem"]
categories = ["network-programming", "cryptography", "no-std"]

[features]
default = ["compression-zstd", "std", "bindings"]

# UniFFI bindings surface. Pulled in by `default` so the historical
# native-FFI consumers (Swift / Kotlin / Python / C bindings under
# `tests/bindings/`) compile unchanged. Split out from `std` because
# UniFFI's exported-symbol metadata is incompatible with
# `wasm-component-ld` (the wasm32-wasip2 linker); the WASI guest in
# `core/tests/fixtures/wasi-guest/` therefore opts out of `default`
# and explicitly enables `["std", "wasi-leg"]` only.
bindings = ["std", "dep:uniffi"]

# `uniffi-cli` (SUPPLY-01): the codegen-only surface. Adds uniffi's `cli`
# feature (which drags in `clap` / `cargo_metadata` / `tempfile`) and is the
# `required-features` for the `uniffi-bindgen` binary. Kept OUT of `bindings`
# so a normal lib / server / mobile build links the FFI scaffolding WITHOUT the
# build-time CLI dependency tree. The `tests/bindings/generate_*.sh` scripts
# enable it when they regenerate the language bindings.
uniffi-cli = ["bindings", "uniffi/cli"]

# `std` (Phase 3.6): the standard "everything on" build. Pulls every std-bound
# optional dep into the graph and unlocks all top-level modules (`api`, `runtime`,
# `transport/legs/{tcp,kcp,faketls,websocket}`, `networks`, `config`, `validation`,
# `crypto/*`, `security/*`, std-bound parts of `transport/*`). Default-on so the
# historical surface keeps compiling unchanged.
#
# Bare-metal targets pass `--no-default-features --features embedded,no-std` to
# drop this entirely.
std = [
    "dep:tokio",
    "dep:tokio-util",
    "dep:futures",
    "dep:async-trait",
    "dep:parking_lot",
    "dep:dashmap",
    "dep:arc-swap",
    "dep:env_logger",
    "dep:tracing",
    "dep:time",
    "dep:argon2",
    "dep:crossbeam-queue",
    "dep:crossbeam-utils",
    "dep:lz4_flex",
    "dep:base64",
    "dep:once_cell",
    "dep:anyhow",
    "dep:thiserror",
    "dep:rand",
    "dep:ring",
    "dep:x25519-dalek",
    "dep:ed25519-dalek",
    "dep:ml-kem",
    "dep:ml-dsa",
    "dep:log",
    "dep:hmac",
    "dep:borsh",
    "dep:hex",
    "dep:bitflags",
    "dep:zeroize",
    "dep:subtle",
    "dep:blake3",
    "dep:hkdf",
    "dep:sha2",
    "dep:getrandom",
    # Native (non-wasm32) optional deps; activating these on a wasm32 target
    # is a no-op because the deps themselves are gated by the
    # `[target.'cfg(not(target_arch = "wasm32"))']` table.
    "dep:kcp-tokio",
    "dep:socket2",
    "bytes/std",
    "serde/std",
]

# zstd compression. Disabled when building for WASM / embedded targets where
# the C bindings in `zstd-sys` are unavailable. lz4_flex (pure-Rust) remains
# the always-on fallback. Implies `std`.
compression-zstd = ["dep:zstd", "std"]

# `EmbeddedLeg` transport over `embedded-io-async` byte streams (Phase 3.4).
# Pure-Rust + no_std-friendly; safe to enable on the host for `cargo test`.
embedded = ["dep:embedded-io-async", "dep:async-lock"]

# `no-std` foundation (Phase 3.6). The audit-gate marker. When ON, the
# in-subset modules (`errors`, `transport::session_transport`, `transport::legs::
# embedded`) compile with `#![no_std] + extern crate alloc`. Whole-crate
# bare-metal compilation additionally requires `--no-default-features` to drop
# the `std` feature and all the std-bound optional deps it gates.
no-std = []

# FIPS 140-3 posture. Pulls in `aws-lc-rs` (FIPS-validated AWS-LC
# bindings) as the substrate for the FIPS-approved primitive set:
#
#   - classical KEM half: X25519 → ECDH-P-256
#   - AEAD: `ring` AES-GCM → `aws-lc-rs`
#   - ChaCha20-Poly1305 dropped (not FIPS-approved)
#   - KDF: blake3 → HKDF-SHA256 everywhere blake3 is used as a KDF
#   - RNG: `aws-lc-rs::rand` (SP 800-90A CTR_DRBG) instead of getrandom
#   - `crypto::self_tests::run_post()` wired into the bind/connect bootstrap
#
# Implies `std`; mutually exclusive with `no-std` (enforced by a
# `compile_error!` in `core/src/lib.rs`). Native-only — `aws-lc-rs`
# does not support wasm32 targets. See `docs/compliance/fips-readiness.md`.
fips = ["std", "dep:aws-lc-rs"]

# WasiLeg transport + WasiRuntime for `wasm32-wasi*` targets (Section B
# of the pre-1.0 deferred-followups plan). Pulls in the `wasi` crate
# (>= 0.14, Preview 2 stable) only on `cfg(target_os = "wasi")`, so
# enabling this feature on non-WASI hosts is a no-op at the dep level.
# Mutually exclusive with `wasm32-unknown-unknown` (browser) — the
# `wasi` crate's WIT bindings do not target the browser; a
# `compile_error!` in `core/src/lib.rs` rejects the combination
# explicitly.
wasi-leg = ["std", "dep:wasi"]

# OpenTelemetry pipeline (Phase 8). Off by default; the `core` library exposes
# OTel `Counter` / `Histogram` / `UpDownCounter` instruments to the embedder
# when this is on. The library never installs a `MeterProvider` /
# `TracerProvider` — that's the embedder's job (see `server/src/telemetry.rs`).
# Implies `std`; mutually compatible with `compression-zstd`. Pulls
# `opentelemetry` + `opentelemetry_sdk` + `tracing-opentelemetry` so
# `tracing` spans bridge into OTel traces.
telemetry-otel = [
    "std",
    "dep:opentelemetry",
    "dep:opentelemetry_sdk",
    "dep:tracing-opentelemetry",
]

[dependencies]
# --- Async & Runtime (std-only) ---
# `tokio` is split per target (Phase 3.5 conditional compilation matrix):
# - Cross-target features: sync primitives, time, io-util, macros, single-thread
#   runtime. These work on both native and wasm32.
# - Native-only features (`net`, `rt-multi-thread`, `signal`, `process`, `fs`,
#   `io-std`) are added in the `[target.'cfg(not(target_arch = "wasm32"))']`
#   block at the bottom of this file.
tokio = { version = "1.36", default-features = false, optional = true, features = [
    "sync", "macros", "rt", "time", "io-util",
] }
tokio-util = { version = "0.7", optional = true, features = ["codec"] }
futures = { version = "0.3", optional = true }
async-trait = { version = "0.1", optional = true }

# --- Serialization & Utils ---
# `bytes` and `serde` have first-class no_std modes (`default-features = false`
# + the `alloc` feature). Kept as required deps so the no-std subset can use
# them; the `std` feature wires the `std` cargo feature back in for richer
# `std::io::Read`-style integration.
bytes = { version = "1", default-features = false, features = ["serde"] }
serde = { version = "1", default-features = false, features = ["derive", "alloc"] }
# Wire-stability pin. `borsh` encodes the handshake messages (the packet header
# + `PhantomPacket` use a hand-rolled big-endian codec, `PacketHeader::to_wire`,
# so they have no serialization dependency). The exact serialized bytes are
# frozen by `core/tests/wire_vectors.rs` (and the transcript hash by
# `transport::handshake::tests::transcript_hash_wire_vector`). A patch/minor bump
# to `borsh` can silently shift the handshake bytes and break interop with no
# `WIRE_VERSION` change, so it is pinned to an exact version — bump it only as a
# deliberate wire change, paired with a vector regeneration.
borsh = { version = "=1.6.1", default-features = false, optional = true, features = ["derive"] }
hex = { version = "0.4", default-features = false, optional = true, features = ["alloc"] }
base64 = { version = "0.22", default-features = false, optional = true, features = ["alloc"] }
anyhow = { version = "1", optional = true }
thiserror = { version = "2", optional = true }
log = { version = "0.4", optional = true }
env_logger = { version = "0.11", optional = true }
rand = { version = "0.8", optional = true }
once_cell = { version = "1.18", optional = true }
subtle = { version = "2.5", default-features = false, optional = true }  # Constant-time equality for secrets (cookies, tokens)
tracing = { version = "0.1", optional = true }  # Structured spans / events for observability (Phase 4.5)

# --- Crypto (std-only) ---
x25519-dalek = { version = "2", optional = true, features = ["static_secrets", "zeroize"] }
ed25519-dalek = { version = "2", optional = true, features = ["zeroize", "rand_core"] }
ring = { version = "0.17", optional = true }  # HW-accelerated AES-GCM (ARM64 AES intrinsics / x86 AES-NI) + ChaCha20-Poly1305
# Phase 5.1: switched from `pqcrypto-kyber` / `pqcrypto-dilithium` (which
# wrap NIST PQC round-3 C reference impls and require libc) to the
# RustCrypto pure-Rust FIPS-203 / FIPS-204 implementations. These compile
# on every target (server, mobile, embedded, wasm32) without C bindings.
# Wire-incompatible with the prior build — bytes laid out per FIPS, not
# per NIST round-3.
# `deterministic` exposes `generate_deterministic` / `encapsulate_deterministic`
# (additive API; no change to the production random path). Used by the NIST KAT
# test (`core/tests/nist_kat.rs`) to byte-match published FIPS-203 vectors.
ml-kem = { version = "0.2", optional = true, features = ["deterministic"] }
ml-dsa = { version = "0.1.1", optional = true }   # stable release; a release candidate must not ship in a 1.0
# `signature` traits are re-exported via `ml_dsa::{Signer, Verifier}` from
# the version ml-dsa pins (3.x); we do not need a direct dependency here.
hkdf = { version = "0.13", optional = true }
sha2 = { version = "0.11", optional = true }
blake3 = { version = "1.5", optional = true }
getrandom = { version = "0.2", optional = true }
bitflags = { version = "2.13", optional = true }
parking_lot = { version = "0.12", optional = true }  # Fast mutex for buffer pool
arc-swap = { version = "1", optional = true }        # Lock-free atomic swap for the CryptoState rekey path (Phase 1.5)
lz4_flex = { version = "0.13.1", optional = true }   # Fastest pure-Rust LZ4 (safe, no C bindings); >=0.13.1 floor avoids yanked 0.11.5 (RUSTSEC-2026-0041)
# zstd pinned to a release version. Optional (gated by `compression-zstd`
# feature) so WASM / embedded builds can opt out of the C bindings in
# zstd-sys, falling back to lz4_flex.
zstd = { version = "0.13", optional = true }

# --- Networking ---
# TCP/UDP-using crates are non-wasm-only (Phase 3.5). Moved to the
# `[target.'cfg(not(target_arch = "wasm32"))']` block at the bottom.
time = { version = "0.3", optional = true }

# --- FFI ---
uniffi = { version = "0.31", optional = true, features = ["tokio"] }
zeroize = { version = "1.8.2", optional = true, features = ["derive"] }
argon2 = { version = "0.5.3", optional = true }
crossbeam-queue = { version = "0.3.12", optional = true }
crossbeam-utils = { version = "0.8", optional = true }  # CachePadded for observability hot-path atomics

# --- OpenTelemetry (optional, behind the `telemetry-otel` feature, Phase 8) ---
# Library-side: only the API + SDK crates. The OTLP exporter and any tonic
# transport live in `server/` (or any embedder), keeping the core library
# transport-runtime-agnostic and matching the existing "core defines, server
# wires" boundary used for HTTP metrics today.
opentelemetry = { version = "0.32", optional = true, default-features = false, features = ["metrics", "trace"] }
opentelemetry_sdk = { version = "0.32", optional = true, default-features = false, features = ["metrics", "trace", "rt-tokio"] }
tracing-opentelemetry = { version = "0.33", optional = true, default-features = false, features = ["tracing-log"] }
hmac = { version = "0.13", optional = true }
dashmap = { version = "6.1.0", optional = true }

# --- Embedded transport (optional, behind the `embedded` feature) ---
# Pure-Rust, no_std + alloc. `embedded-io-async` 0.6 is the byte-stream trait
# the `EmbeddedLeg` is generic over — 0.6 keeps MSRV 1.75, 0.7 bumped it to
# 1.81. `async-lock` is the async mutex guarding the split read/write handles;
# chosen over `embassy-sync` because the latter drags in `heapless` (MSRV 1.87)
# and would need a link-time `critical-section` impl. `async-lock` >= 3.4
# bumped its MSRV to 1.85, so it is capped at `< 3.4` to hold the crate MSRV of
# 1.75 — relax this cap once the crate MSRV moves.
embedded-io-async = { version = "0.6", optional = true }
async-lock = { version = ">=3, <3.5", optional = true, default-features = false }

[build-dependencies]
uniffi = { version = "0.31", features = ["build"] }

# ─── Native (non-wasm32) dependencies (Phase 3.5) ────────────────────────
# TCP/UDP transport legs and raw sockets live here. wasm32 builds
# skip these entirely and use the `WebSocketLeg` instead (see below).
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { version = "1.36", optional = true, features = [
    "net", "rt-multi-thread", "signal", "process", "fs", "io-std",
] }
kcp-tokio = { version = "0.6", optional = true }
# socket2 gives portable access to SO_REUSEPORT (Phase 2.9 — Linux
# multi-accept). Already in the transitive tree via tokio so this is
# free from a build-size standpoint.
socket2 = { version = "0.6", optional = true }
# `aws-lc-rs` is the FIPS-validated substrate behind the `fips` Cargo
# feature (AES-256-GCM, ECDH-P-256, HKDF-SHA256, CTR_DRBG). Pinned to
# 1.x, declared only in this native-only target block because the
# AWS-LC C library + bindgen pipeline does not target wasm32. macOS
# hosts may need `brew install pkg-config openssl@3` for the first
# build (see CONTRIBUTING.md).
#
# Feature selection: `default-features = false` + `features = ["fips"]`.
# In aws-lc-rs 1.17, the `fips` feature is mutually exclusive with the
# default `aws-lc-sys` backend — it activates `dep:aws-lc-fips-sys`
# (AWS-LC-FIPS 3.0.x) as the sole backend. Listing both `aws-lc-sys`
# and `fips` would attempt to enable both backends and bloat the link.
aws-lc-rs = { version = "1.17", optional = true, default-features = false, features = ["fips"] }

# ─── Linux-only dependencies ──────────────────────────────────────────────
# `libc` backs the Linux UDP fast paths in `transport/udp_transport` — the
# `SO_MAX_PACING_RATE` pacing offload (`setsockopt`) and the `sendmmsg` batch
# send — all gated on `cfg(target_os = "linux")`. Declared in a Linux-only
# target block so the direct dependency edge matches the only place the code
# names `libc::`; macOS / Windows / wasm / WASI builds never pull it here.
[target.'cfg(target_os = "linux")'.dependencies]
libc = "0.2"

# ─── Browser-WASM-only dependencies (Phase 3.3) ───────────────────────────
# Pulled in only for `wasm32-unknown-unknown` builds (the browser target).
# Non-WASM hosts and WASI targets (wasm32-wasi*) never see these crates in
# their dependency graph.
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
js-sys = "0.3"
web-sys = { version = "0.3", features = [
    "BinaryType",
    "Blob",
    "CloseEvent",
    "console",
    "ErrorEvent",
    "Event",
    "MessageEvent",
    "WebSocket",
] }
# Phase 3.8 partial: enable getrandom's "js" feature for wasm32-unknown-
# unknown so it falls back to `crypto.getRandomValues` rather than
# emitting a compile_error!. Non-wasm targets and WASI targets ignore
# this — it's a no-op there.
getrandom = { version = "0.2", features = ["js"] }
# Force-enable the `wasm_js` feature on getrandom 0.4 for wasm32-unknown-
# unknown. getrandom 0.4 is pulled in transitively by `ml-dsa →
# crypto-common 0.2` and `uniffi → tempfile`. The wasm path requires the
# `wasm_js` Cargo feature (not a --cfg rustflag) to compile in the
# js-sys/wasm-bindgen backend. WASI builds get the default getrandom path
# (which uses WASI's `random_get`).
getrandom04 = { package = "getrandom", version = "0.4", features = ["wasm_js"] }

# ─── WASI-only dependencies (Section B — wasi-leg) ────────────────────────
# WASI Preview 2 bindings for the WasiLeg transport + WasiRuntime. Lives
# under `cfg(target_os = "wasi")` so it is pulled in only when the
# `wasi-leg` Cargo feature is active AND the build target is a WASI
# triple (wasm32-wasi, wasm32-wasip1, wasm32-wasip2). Non-WASI builds
# (including wasm32-unknown-unknown) never see this crate in their graph.
[target.'cfg(target_os = "wasi")'.dependencies]
wasi = { version = "0.14", optional = true }

[dev-dependencies]
blake3 = "1.5"
ed25519-dalek = { version = "2", features = ["rand_core"] }
rand = "0.8"
uniffi = { version = "0.31", features = ["bindgen-tests"] }
criterion = { version = "0.5", features = ["html_reports", "async_tokio"] }
tokio-test = "0.4"
proptest = "1"  # property-based testing harness
serde_json = "1"  # parse the committed NIST ACVP KAT vectors (core/tests/nist_kat.rs)

# UniFFI codegen entry point. Gated on `uniffi-cli` (SUPPLY-01) so a default
# `cargo build` skips it entirely — the binary needs uniffi's `cli` feature for
# `uniffi_bindgen_main`, and that feature (clap / cargo_metadata / tempfile)
# must not leak into the runtime library / server / mobile artifacts.
[[bin]]
name = "uniffi-bindgen"
path = "src/bin/uniffi-bindgen.rs"
required-features = ["uniffi-cli"]

[[bench]]
name = "transport_bench"
harness = false

[[bench]]
name = "buffer_pool_bench"
harness = false

[[bench]]
name = "syn_flood_bench"
harness = false

[[bench]]
name = "protocol_comparison"
harness = false

[[bench]]
name = "observability_bench"
harness = false

# `embedded_demo` depends on `embedded-io-async`, which is only pulled in by the
# `embedded` feature — gate it so the default `cargo test` / `cargo build
# --examples` skip it instead of failing to resolve the crate.
[[example]]
name = "embedded_demo"
required-features = ["embedded"]

# NOTE: `[profile.release]` lives in the workspace root `Cargo.toml`. Declaring
# release profiles in non-root members is silently ignored by cargo (see warning
# from `cargo check`). Keep this comment as a reminder for future maintainers.

[lib]
name = "phantom_protocol"
crate-type = ["lib", "cdylib"]