diff --git a/CHANGELOG.md b/CHANGELOG.md index 61ae4da..c1ab8ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **security**: control-plane admin authentication (`BARBACANE_CONTROL_ADMIN_TOKEN`), CORS allowlist (`BARBACANE_CONTROL_ALLOWED_ORIGINS`). +- **security**: `.bca` Ed25519 signing (`BARBACANE_SIGNING_KEY`) and verify-on-load against a pinned key (`BARBACANE_TRUSTED_PUBKEY`), plus per-plugin/spec/route checksum verification on load. +- **security**: WASM plugin SSRF guard (blocks loopback/link-local/private/metadata egress; `BARBACANE_ALLOW_INTERNAL_EGRESS` to override) and redirect following disabled. +- **security**: WASM plugin capability enforcement — plugins may only import host functions covered by the capabilities declared in `plugin.toml`. +- **security**: `jwt-auth` performs real signature verification via the host `verify_signature` capability (inline JWK). +- **security**: a security testing framework — adversarial integration suite (`crates/barbacane-test/tests/security/`) and `cargo-fuzz` targets (`fuzz/`). +- **docs**: [Configuration & environment variables](reference/configuration.md) reference. + +### Changed (breaking, secure-by-default) + +- The control plane refuses to start without `BARBACANE_CONTROL_ADMIN_TOKEN`, and all API routes (except `/health` and the data-plane WebSocket) require the bearer token. +- `file://` secret references require `BARBACANE_SECRETS_DIR` and are confined to it. +- MCP requires a valid session for non-`initialize` requests. +- Plugin HTTP egress to internal/metadata addresses is blocked by default. + +### Fixed + +- **security**: fail-open middleware short-circuit downgrade in the WASM chain. +- **security**: panic on hostile `x-request-id` / `traceparent`; unbounded Prometheus path-label cardinality on unmatched routes. +- **deps**: bump `anyhow` to 1.0.103 (RUSTSEC-2026-0190). + ## [0.7.0] - 2026-05-05 Headline: AI gateway extensions land — caller-owned model, glob-based dynamic routing, stateless Responses API, aggregated model catalog, and four new policy middlewares (ADR-0024 + ADR-0030). diff --git a/Cargo.lock b/Cargo.lock index 3496921..fb0b6ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -113,9 +113,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "arbitrary" @@ -444,6 +444,7 @@ dependencies = [ "hex", "home", "reqwest", + "ring", "serde", "serde_json", "serde_yaml", @@ -536,6 +537,7 @@ dependencies = [ "assert_cmd", "barbacane-compiler", "base64", + "flate2", "futures-util", "predicates", "rcgen", @@ -543,6 +545,7 @@ dependencies = [ "rustls", "serde_json", "serde_yaml", + "tar", "tempfile", "thiserror 2.0.18", "tokio", diff --git a/Dockerfile b/Dockerfile index 9f1f53b..2a1aee5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,7 +30,7 @@ FROM gcr.io/distroless/cc-debian12:nonroot # OCI labels for GitHub Container Registry LABEL org.opencontainers.image.source="https://github.com/barbacane-dev/barbacane" LABEL org.opencontainers.image.description="Barbacane API Gateway - Data Plane" -LABEL org.opencontainers.image.licenses="Apache-2.0" +LABEL org.opencontainers.image.licenses="AGPL-3.0-only" # Copy the binary from builder COPY --from=builder /usr/local/bin/barbacane /barbacane diff --git a/Dockerfile.control b/Dockerfile.control index 629a9f7..593488e 100644 --- a/Dockerfile.control +++ b/Dockerfile.control @@ -38,7 +38,7 @@ FROM debian:bookworm-slim # OCI labels for GitHub Container Registry LABEL org.opencontainers.image.source="https://github.com/barbacane-dev/barbacane" LABEL org.opencontainers.image.description="Barbacane API Gateway - Control Plane" -LABEL org.opencontainers.image.licenses="Apache-2.0" +LABEL org.opencontainers.image.licenses="AGPL-3.0-only" # Install runtime dependencies and nginx for serving UI RUN apt-get update && apt-get install -y --no-install-recommends \ diff --git a/Dockerfile.standalone b/Dockerfile.standalone index 8265a4a..7feeb7b 100644 --- a/Dockerfile.standalone +++ b/Dockerfile.standalone @@ -76,10 +76,18 @@ RUN mkdir -p /plugins && \ # --------------------------------------------------------------------------- FROM debian:bookworm-slim +# OCI labels +LABEL org.opencontainers.image.source="https://github.com/barbacane-dev/barbacane" +LABEL org.opencontainers.image.description="Barbacane API Gateway - Standalone" +LABEL org.opencontainers.image.licenses="AGPL-3.0-only" + RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ && rm -rf /var/lib/apt/lists/* +# Create non-root user +RUN groupadd -r barbacane && useradd -r -g barbacane barbacane + # Binary COPY --from=binary-builder /usr/local/bin/barbacane /usr/local/bin/barbacane @@ -90,4 +98,9 @@ COPY --from=plugin-builder /plugins/ /plugins/ COPY docker/plugins.yaml /etc/barbacane/plugins.yaml WORKDIR /workspace +RUN chown -R barbacane:barbacane /workspace + +# Run as non-root (gateway listens on a high port; no privileged bind needed) +USER barbacane + ENTRYPOINT ["barbacane"] diff --git a/Makefile b/Makefile index 8d40519..f042379 100644 --- a/Makefile +++ b/Makefile @@ -78,7 +78,7 @@ clean: # Test & Lint # ----------------------------------------------------------------------------- -.PHONY: test test-verbose test-one check clippy fmt fmt-check +.PHONY: test test-verbose test-one check clippy fmt fmt-check security-test security-test-build fuzz-build test: cargo test --workspace @@ -89,6 +89,24 @@ test-verbose: test-one: cargo test --workspace $(TEST) -- --nocapture +# Adversarial security suite (Layer 1). Tests assert hardened behaviour, so they +# are RED until each finding is fixed. Needs the binaries the harness drives: +# `barbacane` (data plane), `barbacane-control` (control plane, needs Postgres + +# DATABASE_URL), and the WASM plugins (`make plugins`). +# See docs/contributing/security-testing.md. +security-test: plugins + cargo build -p barbacane -p barbacane-control + cargo test -p barbacane-test --test security -- --nocapture + +# Compile-only check of the security suite (does not require running services). +security-test-build: + cargo test -p barbacane-test --test security --no-run + +# Build (not run) the standalone cargo-fuzz targets on stable, to catch bit-rot. +# Actually fuzzing needs nightly + cargo-fuzz: `cd fuzz && cargo +nightly fuzz run `. +fuzz-build: + cd fuzz && cargo build --bins + check: fmt-check clippy clippy: @@ -189,6 +207,8 @@ help: @echo " make release Build release" @echo " make test Run all tests" @echo " make test-verbose Run tests with output" + @echo " make security-test Run the adversarial security suite (RED until fixed)" + @echo " make fuzz-build Build the cargo-fuzz targets (run with +nightly)" @echo " make test-one TEST=name" @echo " make check Run fmt-check + clippy" @echo " make clippy Run clippy lints" diff --git a/ROADMAP.md b/ROADMAP.md index 484ab9f..10d20d8 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -8,7 +8,22 @@ Forward-looking priorities for Barbacane. See [CHANGELOG.md](CHANGELOG.md) for w Actively being worked on: -- _(open — pick up from Next)_ +### Security hardening (in progress) + +A security review drove a hardening pass. Landed on `security/hardening-and-test-framework`: + +- [x] **Control-plane authentication** — admin bearer token (`BARBACANE_CONTROL_ADMIN_TOKEN`) required on all routes except `/health` and the data-plane WebSocket; server fails closed without it. CORS tightened to an allowlist (`BARBACANE_CONTROL_ALLOWED_ORIGINS`). +- [x] **Artifact integrity & signing** — `.bca` now carries an Ed25519 signature over its content hash; the data plane recomputes every plugin/spec/route checksum and verifies the signature against a pinned key (`BARBACANE_TRUSTED_PUBKEY`) on load. Sign at compile with `BARBACANE_SIGNING_KEY`. +- [x] **MCP session enforcement** — non-`initialize` requests without a valid session are rejected (no more header-omission bypass). +- [x] **Plugin egress SSRF guard** — the WASM host HTTP client rejects loopback/link-local/private/metadata targets and no longer follows redirects (override with `BARBACANE_ALLOW_INTERNAL_EGRESS`). +- [x] **`jwt-auth` real verification** — signatures verified via the host `verify_signature` capability (inline JWK); the test-only skip flag is ignored in production builds. +- [x] **Secret confinement** — `file://` secret references confined to `BARBACANE_SECRETS_DIR`. +- [x] **Security test framework** — adversarial integration suite (`crates/barbacane-test/tests/security/`) + `cargo-fuzz` targets (`fuzz/`). See `docs/contributing/security-testing.md`. +- [x] **WASM capability enforcement** — all 33 official `plugin.toml`s migrated to the canonical `host_functions` dialect (verified against each plugin's real wasm imports); the data plane runs `validate_imports` on load and rejects any plugin importing a host function outside its declared capabilities. Enforced when the artifact's capabilities are authoritative (`capabilities_enforced`), i.e. compiled from `plugin.toml`. + +In flight: + +- [ ] **Control-plane capability persistence** — the registry does not yet store plugin capabilities, so control-plane-compiled artifacts are capability-less and load without enforcement. Add a capabilities column + parse `plugin.toml` on plugin upload/seed so control-plane artifacts are enforced too. --- @@ -51,7 +66,7 @@ Committed but not yet scheduled. Grouped by concern. | RBAC | P2 | Role-based access control for control plane API | | Plugin registry | P2 | Central registry for discovering and versioning plugins | | Data plane groups | P2 | Deploy to specific subsets of data planes | -| Artifact signing | P2 | GPG/private-key signing + verification on load | +| Artifact signing | ✅ | Ed25519 signing + verify-on-load shipped (security hardening); remaining: key distribution/rotation tooling, optional Sigstore/transparency-log | | Health metrics collection | P2 | Aggregate CPU, memory, request rates from data planes | | Multi-tenancy | P3 | Organization/team isolation with SNI-based routing | @@ -112,6 +127,12 @@ The first three rungs of the trusted spec-to-run pipeline are shipped (artifact | E1032 validation | P2 | Warn on OpenAPI security scheme without matching auth middleware | | OPA WASM compilation | P1 | Define OPA version, compilation flags, error handling | | Auth plugin auditing | P1 | Security review process for auth plugins | +| Ingress timeouts & limits | P1 | Header-read/idle/handshake timeouts, connection cap, streaming body-size limit on the data plane (wire the parsed `--keepalive-timeout`) | +| Per-plugin secret & capability scope | P1 | Scope resolved secrets per plugin; complete the per-plugin default-deny host-function linker + migrate official `plugin.toml` capability dialects | +| Plugin SDK hardening helpers | P2 | Shared SDK helpers: constant-time secret compare, trusted-proxy client-IP extraction, RFC-9457 errors, CRLF-safe headers (removes per-plugin drift) | +| WASM execution budget | P2 | Epoch-interruption wall-clock deadline + host-imposed timeouts/body caps on blocking host calls | +| CI/supply-chain hardening | P2 | SHA-pin third-party Actions, scope job token permissions, `cargo deny check` (licenses/bans/sources) as a gate, SBOM + image signing | +| Control-plane container non-root | P2 | Run `barbacane-control` (nginx) as non-root (high port or capability) | | Trace volume guidance | P1 | Documentation for managing trace volume at scale | | Integration tests | P2 | Full control plane API lifecycle tests with PostgreSQL | | Compile safety CI | P2 | Fitness functions: deterministic build verification, fuzz testing | diff --git a/crates/barbacane-compiler/Cargo.toml b/crates/barbacane-compiler/Cargo.toml index 8c0e37f..84c340c 100644 --- a/crates/barbacane-compiler/Cargo.toml +++ b/crates/barbacane-compiler/Cargo.toml @@ -13,6 +13,7 @@ categories.workspace = true chrono = { workspace = true } hex = { workspace = true } home = "0.5" +ring = { workspace = true } reqwest = { workspace = true, features = ["blocking"] } serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/barbacane-compiler/src/artifact.rs b/crates/barbacane-compiler/src/artifact.rs index 8db0241..bc02944 100644 --- a/crates/barbacane-compiler/src/artifact.rs +++ b/crates/barbacane-compiler/src/artifact.rs @@ -20,7 +20,12 @@ use crate::error::{CompileError, CompileWarning}; use crate::manifest::ProjectManifest; /// Current artifact format version. -pub const ARTIFACT_VERSION: u32 = 3; +/// +/// v4 adds Ed25519 signing fields and records each plugin's declared capability +/// `host_functions` in the manifest. Whether those capabilities are enforced on +/// load is gated by the manifest `capabilities_enforced` flag (WA-1), not by the +/// version, so older artifacts and capability-less builds load without rejection. +pub const ARTIFACT_VERSION: u32 = 4; /// Options for compilation. #[derive(Debug, Clone)] @@ -96,6 +101,19 @@ pub struct Manifest { /// MCP server configuration (from root-level x-barbacane-mcp). #[serde(default)] pub mcp: McpConfig, + /// Detached Ed25519 signature (hex) over `artifact_hash`. Present when the + /// artifact was signed at compile time (AR-1). Excluded from `artifact_hash`. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub signature: Option, + /// Hex-encoded Ed25519 public key the signature was produced with + /// (informational; verification uses the operator's pinned trusted key). + #[serde(default, skip_serializing_if = "Option::is_none")] + pub signing_public_key: Option, + /// Whether per-plugin `capabilities.host_functions` are authoritative (read + /// from plugin.toml). The data plane enforces the capability contract on + /// load only when this is true (WA-1). + #[serde(default)] + pub capabilities_enforced: bool, } /// MCP server configuration extracted from `x-barbacane-mcp`. @@ -143,6 +161,10 @@ pub struct PluginCapabilities { /// Whether the middleware receives the request body in `on_request`. #[serde(default)] pub body_access: bool, + /// Declared capability host-function names from plugin.toml. The data plane + /// enforces these against the module's actual imports at load (WA-1). + #[serde(default)] + pub host_functions: Vec, } /// A plugin loaded from a .bca artifact, ready for compilation. @@ -237,7 +259,11 @@ pub fn compile( options: &CompileOptions, ) -> Result { let specs = parse_specs(spec_paths)?; - compile_inner(&specs, plugins, output, options) + // Plain `compile` receives caller-built bundles whose declared capabilities + // may not have been read from plugin.toml (e.g. the control plane builds + // bundles from the registry, which does not yet persist capabilities), so + // the resulting artifact is not marked capability-authoritative. + compile_inner(&specs, plugins, output, options, false) } /// Compile specs with a project manifest into a .bca artifact. @@ -280,10 +306,13 @@ pub fn compile_with_manifest( plugin_type: p.plugin_type.unwrap_or_else(|| "plugin".to_string()), wasm_bytes: p.wasm_bytes, body_access: p.body_access, + host_functions: p.host_functions, }) .collect(); - compile_inner(&specs, &plugin_bundles, output, options) + // Bundles were resolved from plugin.toml, so their declared capabilities are + // authoritative and the artifact is eligible for load-time enforcement. + compile_inner(&specs, &plugin_bundles, output, options, true) } /// Load a manifest from a .bca artifact. @@ -411,6 +440,8 @@ pub struct PluginBundle { pub wasm_bytes: Vec, /// Whether this plugin needs the request body. pub body_access: bool, + /// Declared capability host-function names from plugin.toml. + pub host_functions: Vec, } /// Parse spec files into (ApiSpec, content, sha256) tuples. @@ -455,6 +486,7 @@ fn compile_inner( plugins: &[PluginBundle], output: &Path, options: &CompileOptions, + capabilities_authoritative: bool, ) -> Result { let mut warnings: Vec = Vec::new(); let mut operations: Vec = Vec::new(); @@ -683,6 +715,7 @@ fn compile_inner( sha256, capabilities: PluginCapabilities { body_access: plugin.body_access, + host_functions: plugin.host_functions.clone(), }, }); } @@ -731,7 +764,7 @@ fn compile_inner( cfg }; - let manifest = Manifest { + let mut manifest = Manifest { barbacane_artifact_version: ARTIFACT_VERSION, compiled_at: now_utc_iso8601(), compiler_version: COMPILER_VERSION.to_string(), @@ -742,8 +775,16 @@ fn compile_inner( artifact_hash, provenance, mcp, + signature: None, + signing_public_key: None, + capabilities_enforced: capabilities_authoritative, }; + // AR-1: sign the artifact when a signing key is configured. The signature + // covers `artifact_hash`, which already binds every spec, route, and plugin + // WASM hash, so a tampered artifact fails verification on load. + sign_manifest_from_env(&mut manifest)?; + let manifest_json = serde_json::to_string_pretty(&manifest)?; // Create the .bca archive (tar.gz) @@ -811,6 +852,126 @@ fn compute_artifact_hash( format!("sha256:{}", hex::encode(hasher.finalize())) } +// --------------------------------------------------------------------------- +// AR-1: artifact integrity & Ed25519 signing +// --------------------------------------------------------------------------- + +/// Errors from artifact integrity / signature verification. +#[derive(Debug, thiserror::Error)] +pub enum IntegrityError { + #[error("artifact hash mismatch: manifest claims {expected}, recomputed {actual}")] + ArtifactHashMismatch { expected: String, actual: String }, + #[error("plugin '{name}' checksum mismatch: manifest {expected}, actual {actual}")] + PluginChecksumMismatch { + name: String, + expected: String, + actual: String, + }, + #[error("plugin '{name}' is not listed in the manifest")] + UnknownPlugin { name: String }, + #[error("artifact is unsigned but a trusted public key is configured")] + MissingSignature, + #[error("invalid key/signature material: {0}")] + InvalidMaterial(String), + #[error("artifact signature verification failed")] + BadSignature, +} + +/// Decode a hex string into bytes (the local `hex` module only encodes). +fn decode_hex(s: &str) -> Result, String> { + let s = s.trim(); + if !s.len().is_multiple_of(2) { + return Err("odd-length hex string".to_string()); + } + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|e| e.to_string())) + .collect() +} + +/// Recompute the combined artifact hash from the manifest's recorded inputs. +pub fn recompute_artifact_hash(manifest: &Manifest) -> String { + compute_artifact_hash(&manifest.source_specs, &manifest.checksums) +} + +/// Verify the manifest's `artifact_hash` is internally consistent with its +/// recorded spec/route/plugin checksums. +pub fn verify_artifact_hash(manifest: &Manifest) -> Result<(), IntegrityError> { + let actual = recompute_artifact_hash(manifest); + if actual != manifest.artifact_hash { + return Err(IntegrityError::ArtifactHashMismatch { + expected: manifest.artifact_hash.clone(), + actual, + }); + } + Ok(()) +} + +/// Verify that a plugin's actual WASM bytes match the SHA-256 recorded in the +/// manifest (detects a swapped/tampered plugin binary). +pub fn verify_plugin_checksum( + manifest: &Manifest, + name: &str, + wasm_bytes: &[u8], +) -> Result<(), IntegrityError> { + let expected = manifest + .plugins + .iter() + .find(|p| p.name == name) + .map(|p| p.sha256.clone()) + .ok_or_else(|| IntegrityError::UnknownPlugin { + name: name.to_string(), + })?; + let actual = compute_sha256(wasm_bytes); + if actual != expected { + return Err(IntegrityError::PluginChecksumMismatch { + name: name.to_string(), + expected, + actual, + }); + } + Ok(()) +} + +/// Verify the artifact's Ed25519 signature over `artifact_hash` against a pinned +/// trusted public key (hex-encoded). Fails closed if the artifact is unsigned. +pub fn verify_artifact_signature( + manifest: &Manifest, + trusted_public_key_hex: &str, +) -> Result<(), IntegrityError> { + let signature_hex = manifest + .signature + .as_ref() + .ok_or(IntegrityError::MissingSignature)?; + let signature = decode_hex(signature_hex) + .map_err(|e| IntegrityError::InvalidMaterial(format!("signature hex: {e}")))?; + let public_key = decode_hex(trusted_public_key_hex.trim()) + .map_err(|e| IntegrityError::InvalidMaterial(format!("trusted public key hex: {e}")))?; + + ring::signature::UnparsedPublicKey::new(&ring::signature::ED25519, public_key) + .verify(manifest.artifact_hash.as_bytes(), &signature) + .map_err(|_| IntegrityError::BadSignature) +} + +/// Sign the manifest's `artifact_hash` when `BARBACANE_SIGNING_KEY` (a path to a +/// PKCS#8 Ed25519 private key) is set. No-op when unset (unsigned artifact). +fn sign_manifest_from_env(manifest: &mut Manifest) -> Result<(), CompileError> { + use ring::signature::KeyPair; + + let key_path = match std::env::var_os("BARBACANE_SIGNING_KEY") { + Some(p) => p, + None => return Ok(()), + }; + let pkcs8 = std::fs::read(&key_path) + .map_err(|e| CompileError::Signing(format!("reading BARBACANE_SIGNING_KEY: {e}")))?; + let key_pair = ring::signature::Ed25519KeyPair::from_pkcs8(&pkcs8) + .map_err(|e| CompileError::Signing(format!("invalid PKCS#8 Ed25519 key: {e}")))?; + let signature = key_pair.sign(manifest.artifact_hash.as_bytes()); + manifest.signature = Some(hex::encode(signature.as_ref())); + manifest.signing_public_key = Some(hex::encode(key_pair.public_key().as_ref())); + Ok(()) +} + /// Add a file to a tar archive from bytes. fn add_file_to_tar( archive: &mut Builder, @@ -1444,6 +1605,7 @@ paths: plugin_type: "middleware".to_string(), wasm_bytes: fake_wasm.clone(), body_access: false, + host_functions: vec![], }]; let result = compile( @@ -2696,4 +2858,116 @@ paths: let (enabled, _) = resolve_mcp_config(&root, None); assert!(enabled.is_none()); } + + // --- AR-1: artifact integrity & signing --- + + fn integrity_test_manifest() -> Manifest { + let mut checksums = BTreeMap::new(); + checksums.insert("routes.json".to_string(), "routehash".to_string()); + checksums.insert( + "plugins/jwt-auth.wasm".to_string(), + compute_sha256(b"wasm-bytes"), + ); + let source_specs = vec![SourceSpec { + file: "api.yaml".to_string(), + sha256: compute_sha256(b"spec"), + spec_type: "openapi".to_string(), + version: "1.0.0".to_string(), + }]; + let artifact_hash = compute_artifact_hash(&source_specs, &checksums); + Manifest { + barbacane_artifact_version: ARTIFACT_VERSION, + compiled_at: "1970-01-01T00:00:00Z".to_string(), + compiler_version: COMPILER_VERSION.to_string(), + source_specs, + routes_count: 1, + checksums, + plugins: vec![BundledPlugin { + name: "jwt-auth".to_string(), + version: "0.1.0".to_string(), + plugin_type: "middleware".to_string(), + wasm_path: "plugins/jwt-auth.wasm".to_string(), + sha256: compute_sha256(b"wasm-bytes"), + capabilities: PluginCapabilities::default(), + }], + artifact_hash, + provenance: Provenance::default(), + mcp: McpConfig::default(), + signature: None, + signing_public_key: None, + capabilities_enforced: false, + } + } + + fn sign_for_test(manifest: &mut Manifest) -> String { + use ring::signature::KeyPair; + let rng = ring::rand::SystemRandom::new(); + let pkcs8 = ring::signature::Ed25519KeyPair::generate_pkcs8(&rng).unwrap(); + let kp = ring::signature::Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap(); + let sig = kp.sign(manifest.artifact_hash.as_bytes()); + manifest.signature = Some(super::hex::encode(sig.as_ref())); + let pubkey_hex = super::hex::encode(kp.public_key().as_ref()); + manifest.signing_public_key = Some(pubkey_hex.clone()); + pubkey_hex + } + + #[test] + fn artifact_hash_verifies_and_detects_tampering() { + let mut manifest = integrity_test_manifest(); + assert!(verify_artifact_hash(&manifest).is_ok()); + + // Tampered checksum (a swapped plugin) breaks the hash consistency. + manifest + .checksums + .insert("plugins/jwt-auth.wasm".to_string(), "deadbeef".to_string()); + assert!(matches!( + verify_artifact_hash(&manifest), + Err(IntegrityError::ArtifactHashMismatch { .. }) + )); + } + + #[test] + fn plugin_checksum_detects_swapped_wasm() { + let manifest = integrity_test_manifest(); + assert!(verify_plugin_checksum(&manifest, "jwt-auth", b"wasm-bytes").is_ok()); + assert!(matches!( + verify_plugin_checksum(&manifest, "jwt-auth", b"evil-bytes"), + Err(IntegrityError::PluginChecksumMismatch { .. }) + )); + assert!(matches!( + verify_plugin_checksum(&manifest, "ghost", b"x"), + Err(IntegrityError::UnknownPlugin { .. }) + )); + } + + #[test] + fn signature_roundtrip_and_rejection() { + let mut manifest = integrity_test_manifest(); + + // Unsigned artifact fails closed when a trusted key is configured. + assert!(matches!( + verify_artifact_signature(&manifest, "00"), + Err(IntegrityError::MissingSignature) + )); + + let pubkey = sign_for_test(&mut manifest); + // Valid signature under the correct key. + assert!(verify_artifact_signature(&manifest, &pubkey).is_ok()); + + // Tampered artifact_hash → signature no longer matches. + let mut tampered = manifest.clone(); + tampered.artifact_hash = "sha256:0000".to_string(); + assert!(matches!( + verify_artifact_signature(&tampered, &pubkey), + Err(IntegrityError::BadSignature) + )); + + // Wrong (attacker) key → rejected. + let mut other = integrity_test_manifest(); + let _ = sign_for_test(&mut other); + assert!(matches!( + verify_artifact_signature(&manifest, other.signing_public_key.as_ref().unwrap()), + Err(IntegrityError::BadSignature) + )); + } } diff --git a/crates/barbacane-compiler/src/error.rs b/crates/barbacane-compiler/src/error.rs index 25b85ec..2d5df7d 100644 --- a/crates/barbacane-compiler/src/error.rs +++ b/crates/barbacane-compiler/src/error.rs @@ -74,4 +74,8 @@ pub enum CompileError { /// JSON serialization error. #[error("JSON error: {0}")] Json(#[from] serde_json::Error), + + /// Artifact signing failed (bad/missing signing key). + #[error("artifact signing error: {0}")] + Signing(String), } diff --git a/crates/barbacane-compiler/src/lib.rs b/crates/barbacane-compiler/src/lib.rs index ea091ab..2b03500 100644 --- a/crates/barbacane-compiler/src/lib.rs +++ b/crates/barbacane-compiler/src/lib.rs @@ -12,9 +12,10 @@ pub mod spec_parser; pub use artifact::{ compile, compile_with_manifest, load_manifest, load_plugins, load_routes, load_specs, - BundledPlugin, CompileOptions, CompileResult, CompiledOperation, CompiledRoutes, LoadedPlugin, - Manifest, McpConfig, PluginBundle, PluginCapabilities, Provenance, SourceSpec, - ARTIFACT_VERSION, COMPILER_VERSION, + recompute_artifact_hash, verify_artifact_hash, verify_artifact_signature, + verify_plugin_checksum, BundledPlugin, CompileOptions, CompileResult, CompiledOperation, + CompiledRoutes, IntegrityError, LoadedPlugin, Manifest, McpConfig, PluginBundle, + PluginCapabilities, Provenance, SourceSpec, ARTIFACT_VERSION, COMPILER_VERSION, }; pub use error::{CompileError, CompileWarning}; pub use manifest::{ diff --git a/crates/barbacane-compiler/src/manifest.rs b/crates/barbacane-compiler/src/manifest.rs index 3419faf..33a2731 100644 --- a/crates/barbacane-compiler/src/manifest.rs +++ b/crates/barbacane-compiler/src/manifest.rs @@ -33,6 +33,8 @@ struct PluginMeta { struct PluginTomlCapabilities { #[serde(default)] body_access: bool, + #[serde(default)] + host_functions: Vec, } /// Plugin metadata extracted from plugin.toml. @@ -40,6 +42,7 @@ struct PluginMetadata { version: String, plugin_type: String, body_access: bool, + host_functions: Vec, } /// Parse plugin metadata from TOML content. @@ -49,6 +52,7 @@ fn parse_plugin_metadata(content: &str) -> Option { version: parsed.plugin.version, plugin_type: parsed.plugin.plugin_type, body_access: parsed.capabilities.body_access, + host_functions: parsed.capabilities.host_functions, }) } @@ -120,6 +124,10 @@ fn resolve_plugin( version: metadata.as_ref().map(|m| m.version.clone()), plugin_type: metadata.as_ref().map(|m| m.plugin_type.clone()), body_access: metadata.as_ref().is_some_and(|m| m.body_access), + host_functions: metadata + .as_ref() + .map(|m| m.host_functions.clone()) + .unwrap_or_default(), }) } @@ -241,6 +249,8 @@ pub struct ResolvedPlugin { pub plugin_type: Option, /// Whether this plugin needs the request body in `on_request`. pub body_access: bool, + /// Declared capability host-function names from plugin.toml. + pub host_functions: Vec, } impl ProjectManifest { diff --git a/crates/barbacane-control/src/api/auth.rs b/crates/barbacane-control/src/api/auth.rs new file mode 100644 index 0000000..d175fb2 --- /dev/null +++ b/crates/barbacane-control/src/api/auth.rs @@ -0,0 +1,106 @@ +//! Admin authentication for the control-plane API. +//! +//! The control plane is an administrative surface: every mutating route can +//! create projects, upload plugin WASM, deploy artifacts, and mint data-plane +//! API keys. It must never be reachable without a credential. +//! +//! A single shared admin bearer token is required on all routes except +//! `/health` (liveness) and `/ws/data-plane` (which authenticates with its own +//! per-data-plane API key). The token is supplied via the +//! `BARBACANE_CONTROL_ADMIN_TOKEN` environment variable; the server refuses to +//! start without it (fail-closed). Comparison is done over SHA-256 digests so +//! request handling does not leak the token through timing. + +use std::sync::Arc; + +use axum::{ + body::Body, + extract::State, + http::{header::AUTHORIZATION, Request, StatusCode}, + middleware::Next, + response::{IntoResponse, Response}, +}; +use sha2::{Digest, Sha256}; + +use crate::error::ProblemDetails; + +/// Authentication policy applied to protected control-plane routes. +#[derive(Clone)] +pub enum AdminAuth { + /// Require a bearer token whose SHA-256 digest matches this value. + Token(Arc<[u8; 32]>), + /// Authentication disabled. **Test only** — the production binary never + /// constructs this variant; `barbacane-control serve` always uses + /// [`AdminAuth::from_token`] and fails to start without a token. Only + /// reachable from `#[cfg(test)]` harnesses, hence the scoped allow. + #[cfg_attr(not(test), allow(dead_code))] + Disabled, +} + +impl AdminAuth { + /// Build a policy that requires the given token. + pub fn from_token(token: &str) -> Self { + AdminAuth::Token(Arc::new(sha256(token.as_bytes()))) + } + + /// Whether `presented` is an acceptable credential under this policy. + fn accepts(&self, presented: &str) -> bool { + match self { + AdminAuth::Disabled => true, + AdminAuth::Token(expected) => ct_eq(&sha256(presented.as_bytes()), expected), + } + } +} + +fn sha256(bytes: &[u8]) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(bytes); + hasher.finalize().into() +} + +/// Constant-time comparison of two fixed-size digests. +fn ct_eq(a: &[u8; 32], b: &[u8; 32]) -> bool { + let mut diff = 0u8; + for i in 0..32 { + diff |= a[i] ^ b[i]; + } + diff == 0 +} + +/// Axum middleware enforcing the admin bearer token. +pub async fn require_admin( + State(auth): State, + req: Request, + next: Next, +) -> Response { + if let AdminAuth::Disabled = auth { + return next.run(req).await; + } + + let authorized = req + .headers() + .get(AUTHORIZATION) + .and_then(|value| value.to_str().ok()) + .and_then(|value| value.strip_prefix("Bearer ")) + .map(|token| auth.accepts(token)) + .unwrap_or(false); + + if authorized { + next.run(req).await + } else { + let mut response = ProblemDetails { + error_type: "urn:barbacane:error:unauthorized".into(), + title: "Unauthorized".into(), + status: StatusCode::UNAUTHORIZED.as_u16(), + detail: Some("a valid admin bearer token is required".into()), + instance: None, + errors: vec![], + } + .into_response(); + response.headers_mut().insert( + axum::http::header::WWW_AUTHENTICATE, + axum::http::HeaderValue::from_static("Bearer"), + ); + response + } +} diff --git a/crates/barbacane-control/src/api/mod.rs b/crates/barbacane-control/src/api/mod.rs index 3cfc047..a98971f 100644 --- a/crates/barbacane-control/src/api/mod.rs +++ b/crates/barbacane-control/src/api/mod.rs @@ -2,6 +2,7 @@ mod api_keys; mod artifacts; +mod auth; mod compilations; mod data_planes; mod health; @@ -15,6 +16,7 @@ mod router; mod specs; pub mod ws; +pub use auth::AdminAuth; pub use router::create_router; pub use ws::ConnectionManager; diff --git a/crates/barbacane-control/src/api/router.rs b/crates/barbacane-control/src/api/router.rs index a617b61..296140b 100644 --- a/crates/barbacane-control/src/api/router.rs +++ b/crates/barbacane-control/src/api/router.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use axum::{ - http::{header, HeaderValue, StatusCode}, + http::{header, HeaderValue, Method, StatusCode}, response::{Html, IntoResponse}, routing::{delete, get, patch, post, put}, Router, @@ -11,7 +11,7 @@ use axum::{ use sqlx::PgPool; use tokio::sync::mpsc; use tower_http::{ - cors::{Any, CorsLayer}, + cors::{AllowOrigin, CorsLayer}, set_header::SetResponseHeaderLayer, trace::TraceLayer, }; @@ -19,6 +19,7 @@ use uuid::Uuid; use scalar_api_reference::scalar_html_default; +use super::auth::{require_admin, AdminAuth}; use super::ws::ConnectionManager; use super::{ api_keys, artifacts, compilations, data_planes, health, init, operations, plugins, @@ -65,11 +66,41 @@ async fn api_docs() -> Html { Html(scalar_html_default(&config)) } +/// Build a CORS layer from the `BARBACANE_CONTROL_ALLOWED_ORIGINS` environment +/// variable (a comma-separated allowlist of origins). When unset or empty, no +/// cross-origin requests are permitted — the admin API is same-origin by +/// default. Credentials are never combined with a wildcard origin. +fn cors_layer() -> CorsLayer { + let origins: Vec = std::env::var("BARBACANE_CONTROL_ALLOWED_ORIGINS") + .unwrap_or_default() + .split(',') + .map(str::trim) + .filter(|s| !s.is_empty()) + .filter_map(|s| HeaderValue::from_str(s).ok()) + .collect(); + + CorsLayer::new() + .allow_origin(AllowOrigin::list(origins)) + .allow_methods([ + Method::GET, + Method::POST, + Method::PUT, + Method::PATCH, + Method::DELETE, + ]) + .allow_headers([header::AUTHORIZATION, header::CONTENT_TYPE]) +} + /// Create the API router with all routes. +/// +/// `admin_auth` is enforced on every route except `/health` and the data-plane +/// WebSocket (`/ws/data-plane`), which authenticates with its own per-data-plane +/// API key. pub fn create_router( pool: PgPool, compilation_tx: Option>, connection_manager: Arc, + admin_auth: AdminAuth, ) -> Router { let state = AppState { pool, @@ -77,12 +108,18 @@ pub fn create_router( connection_manager, }; - Router::new() + // Public routes: liveness, and the data-plane WebSocket (which performs its + // own API-key authentication inside the handler). + let public = Router::new() + .route("/health", get(health::health_check)) + // WebSocket for data plane connections + .route("/ws/data-plane", get(ws::ws_handler)); + + // Protected routes: everything else requires the admin bearer token. + let protected = Router::new() // OpenAPI spec and documentation .route("/openapi", get(openapi_spec)) .route("/docs", get(api_docs)) - // Health - .route("/health", get(health::health_check)) // Init .route("/init", post(init::init_project)) // Specs @@ -194,16 +231,18 @@ pub fn create_router( "/projects/{id}/deploy", post(data_planes::deploy_to_data_planes), ) - // WebSocket for data plane connections - .route("/ws/data-plane", get(ws::ws_handler)) - // Middleware + // Admin authentication on every protected route. + .layer(axum::middleware::from_fn_with_state( + admin_auth, + require_admin, + )); + + Router::new() + .merge(public) + .merge(protected) + // Middleware applied to all routes .layer(TraceLayer::new_for_http()) - .layer( - CorsLayer::new() - .allow_origin(Any) - .allow_methods(Any) - .allow_headers(Any), - ) + .layer(cors_layer()) // API versioning: set Content-Type to versioned media type for JSON responses .layer(SetResponseHeaderLayer::if_not_present( axum::http::header::CONTENT_TYPE, diff --git a/crates/barbacane-control/src/api/tests.rs b/crates/barbacane-control/src/api/tests.rs index 50a1407..50202b3 100644 --- a/crates/barbacane-control/src/api/tests.rs +++ b/crates/barbacane-control/src/api/tests.rs @@ -37,7 +37,12 @@ async fn make_app() -> Option { crate::db::run_migrations(&pool).await.ok()?; let conn_mgr = Arc::new(crate::api::ConnectionManager::new()); - Some(crate::api::create_router(pool, None, conn_mgr)) + Some(crate::api::create_router( + pool, + None, + conn_mgr, + crate::api::AdminAuth::Disabled, + )) } /// Send one request through the router and return the status + body bytes. diff --git a/crates/barbacane-control/src/compiler/worker.rs b/crates/barbacane-control/src/compiler/worker.rs index 29cb158..ad336e1 100644 --- a/crates/barbacane-control/src/compiler/worker.rs +++ b/crates/barbacane-control/src/compiler/worker.rs @@ -262,8 +262,12 @@ async fn resolve_project_plugins( version: plugin_with_binary.version.clone(), plugin_type: plugin_with_binary.plugin_type.clone(), wasm_bytes: plugin_with_binary.wasm_binary, - // TODO: read body_access from DB once the plugins table has a capabilities column + // TODO: read body_access + host_functions from the registry once the + // plugins table persists capabilities. Until then the control plane + // compiles capability-less (non-authoritative) artifacts, so the data + // plane loads them without capability enforcement (WA-1). body_access: false, + host_functions: vec![], }); } diff --git a/crates/barbacane-control/src/main.rs b/crates/barbacane-control/src/main.rs index 90855e1..ef9e7b2 100644 --- a/crates/barbacane-control/src/main.rs +++ b/crates/barbacane-control/src/main.rs @@ -117,6 +117,20 @@ fn main() -> ExitCode { } async fn run_server(listen: SocketAddr, database_url: &str, migrate: bool) -> anyhow::Result<()> { + // Admin authentication is mandatory: the control plane can mint API keys, + // upload plugin WASM, and deploy artifacts, so it must never run without a + // credential. Fail closed if the token is not configured. + let admin_token = std::env::var("BARBACANE_CONTROL_ADMIN_TOKEN") + .ok() + .filter(|t| !t.trim().is_empty()) + .ok_or_else(|| { + anyhow::anyhow!( + "BARBACANE_CONTROL_ADMIN_TOKEN must be set to a non-empty value; \ + the control-plane API refuses to start unauthenticated" + ) + })?; + let admin_auth = api::AdminAuth::from_token(&admin_token); + // Create database pool let pool = db::create_pool(database_url).await?; @@ -129,6 +143,7 @@ async fn run_server(listen: SocketAddr, database_url: &str, migrate: bool) -> an server::run(server::ServerConfig { listen_addr: listen, pool, + admin_auth, }) .await } diff --git a/crates/barbacane-control/src/server.rs b/crates/barbacane-control/src/server.rs index aa19526..a64067b 100644 --- a/crates/barbacane-control/src/server.rs +++ b/crates/barbacane-control/src/server.rs @@ -8,7 +8,7 @@ use tokio::net::TcpListener; use tokio::sync::mpsc; use uuid::Uuid; -use crate::api::{create_router, ConnectionManager}; +use crate::api::{create_router, AdminAuth, ConnectionManager}; use crate::db::DataPlanesRepository; /// How often to check for stale data planes (seconds). @@ -21,6 +21,7 @@ const STALE_THRESHOLD_MINUTES: i64 = 2; pub struct ServerConfig { pub listen_addr: SocketAddr, pub pool: PgPool, + pub admin_auth: AdminAuth, } /// Run the control plane server. @@ -56,7 +57,7 @@ pub async fn run(config: ServerConfig) -> anyhow::Result<()> { let connection_manager = Arc::new(ConnectionManager::new()); // Create router - let app = create_router(config.pool, Some(tx), connection_manager); + let app = create_router(config.pool, Some(tx), connection_manager, config.admin_auth); // Bind and serve let listener = TcpListener::bind(config.listen_addr).await?; diff --git a/crates/barbacane-test/Cargo.toml b/crates/barbacane-test/Cargo.toml index 526c245..6febd3d 100644 --- a/crates/barbacane-test/Cargo.toml +++ b/crates/barbacane-test/Cargo.toml @@ -22,6 +22,10 @@ tokio-tungstenite = { workspace = true } futures-util = { workspace = true } assert_cmd = { workspace = true } predicates = { workspace = true } +# Used by the security suite (artifact_integrity) to rebuild a .bca archive with +# a single tampered byte. Same crates the compiler uses for the .bca format. +flate2 = { workspace = true } +tar = { workspace = true } [lints] workspace = true diff --git a/crates/barbacane-test/SECURITY-TESTING.md b/crates/barbacane-test/SECURITY-TESTING.md new file mode 100644 index 0000000..e56d5c5 --- /dev/null +++ b/crates/barbacane-test/SECURITY-TESTING.md @@ -0,0 +1,21 @@ +# Security testing + +The adversarial security suite for Barbacane lives in this crate under +`tests/security.rs` + `tests/security/` (one module per threat category), with +fixtures in `../../tests/fixtures/security/`. + +Full documentation — threat model, finding IDs, how to run (incl. the Docker / +PostgreSQL requirements), how to run the cargo-fuzz targets in `../../fuzz/`, +and how to add a new security test — is in: + +> **[docs/contributing/security-testing.md](../../docs/contributing/security-testing.md)** + +Quick start: + +```bash +# Compile-only (no services needed): +cargo test -p barbacane-test --test security --no-run + +# Run (RED until findings are fixed — that is intended): +make security-test +``` diff --git a/crates/barbacane-test/tests/security.rs b/crates/barbacane-test/tests/security.rs new file mode 100644 index 0000000..a472956 --- /dev/null +++ b/crates/barbacane-test/tests/security.rs @@ -0,0 +1,93 @@ +//! Adversarial security test suite (Layer 1 of the security testing framework). +//! +//! This single test binary aggregates every security category as a submodule +//! (files under `tests/security/`) so they can share the helpers defined here. +//! Each category file asserts the SECURE / hardened behaviour, so the test is +//! RED today and turns GREEN as the corresponding finding is fixed. Every red +//! test carries a comment of the form +//! `// EXPECTED TO FAIL until is fixed`. +//! +//! Findings covered (see docs/contributing/security-testing.md for the full +//! threat model): +//! +//! | Finding | Category | File | +//! |-----------------|---------------------|---------------------------------| +//! | BARB-SEC-001 | authz / IDOR | security/authz.rs | +//! | BARB-SEC-002 | SSRF | security/ssrf.rs | +//! | BARB-SEC-003 | DoS / resource caps | security/dos.rs | +//! | BARB-SEC-004 | sandbox / capability| security/sandbox.rs | +//! | BARB-SEC-005 | crypto / auth | security/crypto_auth.rs | +//! | BARB-SEC-006 | artifact integrity | security/artifact_integrity.rs | +//! +//! Run with: `cargo test -p barbacane-test --test security` +//! +//! Most categories boot the data-plane gateway (and BARB-SEC-001 boots the +//! control-plane binary), which requires the relevant services to be available. +//! See the docs for the Docker / Postgres requirements. + +// Shared helpers live here at the test-binary root; category modules reach them +// via `super::`. +#![allow(dead_code)] // Helpers are shared across category modules; not all are used by every module. + +use std::path::PathBuf; + +mod security { + pub mod artifact_integrity; + pub mod authz; + pub mod crypto_auth; + pub mod dos; + pub mod sandbox; + pub mod ssrf; +} + +/// Absolute path to a fixture under `tests/fixtures/`. +/// +/// Identical to the `fixture()` helper duplicated across the existing test +/// files, hoisted here so the security modules can share it. +pub fn fixture(name: &str) -> String { + fixtures_dir().join(name).display().to_string() +} + +/// Absolute path to the shared `tests/fixtures` directory. +pub fn fixtures_dir() -> PathBuf { + // CARGO_MANIFEST_DIR = .../crates/barbacane-test + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("crates/") + .parent() + .expect("workspace root") + .join("tests/fixtures") +} + +/// Absolute path to the security-specific fixtures directory +/// (`tests/fixtures/security/`). +pub fn security_fixture(name: &str) -> String { + fixtures_dir() + .join("security") + .join(name) + .display() + .to_string() +} + +/// Current Unix timestamp in seconds. +pub fn now_timestamp() -> u64 { + use std::time::{SystemTime, UNIX_EPOCH}; + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system clock is after the Unix epoch") + .as_secs() +} + +/// Build an unsigned JWT (`header.payload.signature`) from the given header and +/// claims JSON. Used to forge `alg:none`, expired, and tampered tokens. +pub fn encode_jwt( + header: &serde_json::Value, + claims: &serde_json::Value, + signature: &[u8], +) -> String { + use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; + let header_b64 = URL_SAFE_NO_PAD.encode(header.to_string().as_bytes()); + let claims_b64 = URL_SAFE_NO_PAD.encode(claims.to_string().as_bytes()); + let sig_b64 = URL_SAFE_NO_PAD.encode(signature); + format!("{}.{}.{}", header_b64, claims_b64, sig_b64) +} diff --git a/crates/barbacane-test/tests/security/artifact_integrity.rs b/crates/barbacane-test/tests/security/artifact_integrity.rs new file mode 100644 index 0000000..9f9ad89 --- /dev/null +++ b/crates/barbacane-test/tests/security/artifact_integrity.rs @@ -0,0 +1,314 @@ +//! BARB-SEC-006 — Artifact integrity (hash + signature verification on load). +//! +//! Threat: `Gateway::load` (`crates/barbacane/src/main.rs`) reads the manifest, +//! routes, specs, and plugin WASM out of the `.bca` archive but NEVER verifies +//! them against the per-file `checksums` / `artifact_hash` recorded in +//! `manifest.json`, and there is no signature at all. An attacker who can modify +//! an artifact at rest (or in transit to a data plane) can swap in a malicious +//! plugin, rewrite routes, or alter the manifest, and the gateway will load it. +//! +//! The fix: +//! * verify each archive entry's SHA-256 against `manifest.checksums` and the +//! combined `artifact_hash` at load time (reject on mismatch), and +//! * verify an Ed25519 signature over the artifact against a trusted public +//! key provided via `BARBACANE_TRUSTED_PUBKEY` (reject if missing/invalid). +//! +//! ## How these tests work +//! +//! We compile a real `.bca` via `barbacane-compiler`, then rebuild the gzip+tar +//! archive flipping exactly one byte inside a chosen member (plugin WASM, routes, +//! or manifest) — leaving the gzip/tar framing intact so the corruption is +//! *content* corruption, not a parse error. We then ask the actual `barbacane` +//! binary to `serve` the tampered artifact and assert it REFUSES TO START. +//! +//! Today there is no verification, so a content-tampered artifact loads and the +//! gateway comes up healthy → RED. Once hash/signature verification lands, the +//! load fails fast → GREEN. + +use std::collections::BTreeMap; +use std::io::Read; +use std::net::TcpListener; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; +use std::time::Duration; + +use barbacane_compiler::{compile_with_manifest, CompileOptions, ProjectManifest}; +use tempfile::TempDir; + +use crate::fixtures_dir; + +/// Compile `minimal.yaml` (uses only the `mock` plugin) into a `.bca` in `dir`. +fn compile_minimal_artifact(dir: &Path) -> PathBuf { + let fixtures = fixtures_dir(); + let spec = fixtures.join("minimal.yaml"); + let manifest_path = fixtures.join("barbacane.yaml"); + + let project_manifest = + ProjectManifest::load(&manifest_path).expect("load fixtures barbacane.yaml manifest"); + + let artifact = dir.join("artifact.bca"); + let options = CompileOptions { + allow_plaintext: true, + ..CompileOptions::default() + }; + compile_with_manifest( + &[spec.as_path()], + &project_manifest, + &fixtures, + &artifact, + &options, + ) + .expect("compile minimal.yaml"); + artifact +} + +/// Read all entries of a gzip+tar `.bca` into (path, bytes), preserving order. +fn read_archive(path: &Path) -> Vec<(String, Vec)> { + let file = std::fs::File::open(path).expect("open artifact"); + let decoder = flate2::read::GzDecoder::new(file); + let mut archive = tar::Archive::new(decoder); + + let mut entries = Vec::new(); + for entry in archive.entries().expect("read entries") { + let mut entry = entry.expect("entry"); + let name = entry + .path() + .expect("entry path") + .to_string_lossy() + .into_owned(); + let mut bytes = Vec::new(); + entry.read_to_end(&mut bytes).expect("read entry bytes"); + entries.push((name, bytes)); + } + entries +} + +/// Rewrite a gzip+tar `.bca` from the given (path, bytes) entries. +fn write_archive(path: &Path, entries: &[(String, Vec)]) { + let file = std::fs::File::create(path).expect("create tampered artifact"); + let encoder = flate2::write::GzEncoder::new(file, flate2::Compression::default()); + let mut builder = tar::Builder::new(encoder); + + for (name, bytes) in entries { + let mut header = tar::Header::new_gnu(); + header.set_size(bytes.len() as u64); + header.set_mode(0o644); + header.set_cksum(); + builder + .append_data(&mut header, name, bytes.as_slice()) + .expect("append entry"); + } + let encoder = builder.into_inner().expect("finish tar"); + encoder.finish().expect("finish gzip"); +} + +/// Compile an artifact, flip one byte inside the first entry whose path matches +/// `predicate`, and write the result to a new `.bca`. Returns its path. +fn tamper_artifact(dir: &Path, predicate: impl Fn(&str) -> bool) -> PathBuf { + let original = compile_minimal_artifact(dir); + let mut entries = read_archive(&original); + + let target = entries + .iter_mut() + .find(|(name, bytes)| predicate(name) && !bytes.is_empty()) + .expect("found an entry matching the tamper predicate"); + + // Flip one byte in the middle of the chosen member. + let idx = target.1.len() / 2; + target.1[idx] ^= 0xFF; + + let tampered = dir.join("tampered.bca"); + write_archive(&tampered, &entries); + tampered +} + +/// Free TCP port for the gateway under test. +fn free_port() -> u16 { + let l = TcpListener::bind("127.0.0.1:0").expect("bind ephemeral"); + let p = l.local_addr().expect("local addr").port(); + drop(l); + p +} + +/// Locate the built `barbacane` data-plane binary. +fn find_barbacane_binary() -> Option { + [ + "target/debug/barbacane", + "target/release/barbacane", + "../target/debug/barbacane", + "../target/release/barbacane", + "../../target/debug/barbacane", + "../../target/release/barbacane", + ] + .iter() + .map(PathBuf::from) + .find(|p| p.exists()) +} + +/// Boot `barbacane serve` against `artifact` and report whether the gateway +/// becomes healthy. `true` = loaded & serving (verification did NOT reject); +/// `false` = the process refused to start / never became healthy. +async fn gateway_loads_artifact(artifact: &Path) -> bool { + let Some(binary) = find_barbacane_binary() else { + // Caller treats `None`-equivalent as skip; surface via panic-free path. + eprintln!("skip: barbacane binary not built (run `cargo build -p barbacane`)"); + // Returning `false` here would masquerade as "rejected"; instead we make + // the caller skip by checking the binary separately. See callers. + return false; + }; + + let port = free_port(); + let admin_port = free_port(); + + let mut child = Command::new(&binary) + .arg("serve") + .arg("--artifact") + .arg(artifact) + .arg("--listen") + .arg(format!("127.0.0.1:{}", port)) + .arg("--admin-bind") + .arg(format!("127.0.0.1:{}", admin_port)) + .arg("--dev") + .arg("--allow-plaintext-upstream") + .env("BARBACANE_TRUSTED_PUBKEY", "") // no trusted key configured + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("spawn barbacane serve"); + + let client = reqwest::Client::new(); + let health = format!("http://127.0.0.1:{}/__barbacane/health", port); + + let mut healthy = false; + for _ in 0..50 { + if let Ok(resp) = client.get(&health).send().await { + if resp.status().is_success() { + healthy = true; + break; + } + } + if let Ok(Some(_)) = child.try_wait() { + // Process exited before becoming healthy → load rejected. + break; + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + + let _ = child.kill(); + let _ = child.wait(); + healthy +} + +/// Tampering a bundled plugin's WASM bytes must make the load FAIL. +#[tokio::test] +async fn tampered_plugin_wasm_is_rejected() { + // EXPECTED TO FAIL until BARB-SEC-006 is fixed (Gateway::load does not verify + // archive-entry hashes against manifest.checksums or any signature). + if find_barbacane_binary().is_none() { + eprintln!("skip: barbacane binary not built (run `cargo build -p barbacane`)"); + return; + } + let dir = TempDir::new().expect("temp dir"); + let tampered = tamper_artifact(dir.path(), |name| name.ends_with(".wasm")); + + let loaded = gateway_loads_artifact(&tampered).await; + assert!( + !loaded, + "gateway must refuse to load an artifact whose bundled plugin WASM was \ + tampered (hash mismatch / signature invalid)" + ); +} + +/// Tampering `routes.json` must make the load FAIL. +#[tokio::test] +async fn tampered_routes_is_rejected() { + // EXPECTED TO FAIL until BARB-SEC-006 is fixed. + if find_barbacane_binary().is_none() { + eprintln!("skip: barbacane binary not built (run `cargo build -p barbacane`)"); + return; + } + let dir = TempDir::new().expect("temp dir"); + let tampered = tamper_artifact(dir.path(), |name| name == "routes.json"); + + let loaded = gateway_loads_artifact(&tampered).await; + assert!( + !loaded, + "gateway must refuse to load an artifact whose routes.json was tampered" + ); +} + +/// Tampering the `manifest.json` itself must make the load FAIL — verification +/// must not trust the manifest's own recorded hash blindly; a real signature +/// over the manifest is what makes this detectable. +#[tokio::test] +async fn tampered_manifest_is_rejected() { + // EXPECTED TO FAIL until BARB-SEC-006 is fixed (no signature over the + // manifest; an attacker can edit manifest.json and its self-described hash). + if find_barbacane_binary().is_none() { + eprintln!("skip: barbacane binary not built (run `cargo build -p barbacane`)"); + return; + } + let dir = TempDir::new().expect("temp dir"); + + // For the manifest we corrupt a recorded checksum value so that, under the + // fix, the per-entry hash check (or the signature) fails. We rewrite the + // manifest JSON rather than flipping a random byte so the JSON still parses. + let original = compile_minimal_artifact(dir.path()); + let mut entries = read_archive(&original); + for (name, bytes) in entries.iter_mut() { + if name == "manifest.json" { + let mut manifest: serde_json::Value = + serde_json::from_slice(bytes).expect("parse manifest.json"); + // Corrupt one recorded checksum to a plausible-but-wrong value. + if let Some(checksums) = manifest + .get_mut("checksums") + .and_then(|c| c.as_object_mut()) + { + let mut replacement = BTreeMap::new(); + for (k, _v) in checksums.iter() { + replacement.insert( + k.clone(), + "sha256:0000000000000000000000000000000000000000000000000000000000000000" + .to_string(), + ); + } + for (k, v) in replacement { + checksums.insert(k, serde_json::Value::String(v)); + } + } + *bytes = serde_json::to_vec(&manifest).expect("reserialize manifest"); + } + } + let tampered = dir.path().join("tampered.bca"); + write_archive(&tampered, &entries); + + let loaded = gateway_loads_artifact(&tampered).await; + assert!( + !loaded, + "gateway must refuse to load an artifact whose manifest checksums were \ + tampered (or whose signature does not cover/validate the manifest)" + ); +} + +/// Sanity / positive control: the UNtampered freshly-compiled artifact loads +/// fine. This guards against the tamper helpers being so destructive that every +/// artifact fails to load (which would make the RED tests above meaningless). +/// +/// NOTE: once BARB-SEC-006 lands and load requires a valid `BARBACANE_TRUSTED_PUBKEY` +/// signature, this control will need the signing key wired in; until then a +/// pristine artifact must load. +#[tokio::test] +async fn untampered_artifact_loads() { + if find_barbacane_binary().is_none() { + eprintln!("skip: barbacane binary not built (run `cargo build -p barbacane`)"); + return; + } + let dir = TempDir::new().expect("temp dir"); + let artifact = compile_minimal_artifact(dir.path()); + + let loaded = gateway_loads_artifact(&artifact).await; + assert!( + loaded, + "a pristine, untampered artifact must load and the gateway must become healthy" + ); +} diff --git a/crates/barbacane-test/tests/security/authz.rs b/crates/barbacane-test/tests/security/authz.rs new file mode 100644 index 0000000..98a1228 --- /dev/null +++ b/crates/barbacane-test/tests/security/authz.rs @@ -0,0 +1,324 @@ +//! BARB-SEC-001 — Control-plane authorization & project-scoped IDOR. +//! +//! Threat: the control plane is the administrative surface — it can create +//! projects, upload plugin WASM, deploy artifacts, and mint data-plane API +//! keys. Today every route is reachable with no credential, and global +//! `/specs/{id}` / `/artifacts/{id}` reads perform no project-ownership check. +//! +//! The fix (per the hardening plan): +//! * Require a Bearer token on every route except `/health` and +//! `/ws/data-plane`. The token is read from `BARBACANE_CONTROL_ADMIN_TOKEN` +//! and the server fails to start without it. +//! * Enforce project ownership so project A's credential cannot read or +//! mutate project B's specs / artifacts / api-keys (IDOR). +//! +//! The `require_admin` middleware already exists in +//! `crates/barbacane-control/src/api/auth.rs` but is NOT yet wired into +//! `create_router`, so these tests are RED. +//! +//! ## Why these tests boot a subprocess +//! +//! `barbacane-control` is a **binary-only** crate (no `src/lib.rs`) and +//! `barbacane-test` does not depend on it, so we cannot drive its Axum router +//! in-process. We therefore boot the `barbacane-control` binary the same way +//! `TestGateway` boots the data plane. This requires: +//! * a reachable PostgreSQL (`DATABASE_URL`, see docs), and +//! * the `barbacane-control` binary to be built (`cargo build -p barbacane-control`). +//! +//! When either is missing the harness returns `None` and the test SKIPS (prints +//! a `skip:` line and returns) rather than failing spuriously — matching the +//! pattern in `crates/barbacane-control/src/api/tests.rs`. The security +//! assertions only run when the control plane is actually up, so they are RED +//! against a running-but-unhardened control plane and GREEN once the fix lands. + +use std::path::{Path, PathBuf}; +use std::process::{Child, Command, Stdio}; +use std::time::Duration; + +/// A booted control-plane process for authz testing. +struct TestControlPlane { + child: Child, + base_url: String, + client: reqwest::Client, +} + +impl Drop for TestControlPlane { + fn drop(&mut self) { + let _ = self.child.kill(); + let _ = self.child.wait(); + } +} + +impl TestControlPlane { + /// Boot the control plane with the given admin token in the environment. + /// + /// Returns `None` (test should SKIP) when prerequisites are unavailable: + /// no `DATABASE_URL`, no built binary, or the server never becomes healthy. + async fn boot(admin_token: Option<&str>) -> Option { + let database_url = std::env::var("DATABASE_URL").ok()?; + let binary = find_control_binary()?; + let port = free_port()?; + let base_url = format!("http://127.0.0.1:{}", port); + + let mut cmd = Command::new(&binary); + cmd.arg("serve") + .arg("--listen") + .arg(format!("127.0.0.1:{}", port)) + .arg("--database-url") + .arg(&database_url) + .env("DATABASE_URL", &database_url) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + match admin_token { + Some(tok) => { + cmd.env("BARBACANE_CONTROL_ADMIN_TOKEN", tok); + } + None => { + cmd.env_remove("BARBACANE_CONTROL_ADMIN_TOKEN"); + } + } + + let child = cmd.spawn().ok()?; + let client = reqwest::Client::new(); + + let mut cp = TestControlPlane { + child, + base_url, + client, + }; + + // Poll /health until ready (or give up → skip). + let health = format!("{}/health", cp.base_url); + for _ in 0..100 { + if let Ok(resp) = cp.client.get(&health).send().await { + if resp.status().is_success() { + return Some(cp); + } + } + if let Ok(Some(_)) = cp.child.try_wait() { + // Process exited before becoming healthy (e.g. fail-closed on a + // missing admin token, or no DB) — treat as skip. + return None; + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + None + } + + fn url(&self, path: &str) -> String { + format!("{}{}", self.base_url, path) + } +} + +/// Locate the built `barbacane-control` binary. +fn find_control_binary() -> Option { + let candidates = [ + "target/debug/barbacane-control", + "target/release/barbacane-control", + "../target/debug/barbacane-control", + "../target/release/barbacane-control", + "../../target/debug/barbacane-control", + "../../target/release/barbacane-control", + ]; + candidates + .iter() + .map(PathBuf::from) + .find(|p| Path::new(p).exists()) +} + +/// Grab an OS-assigned free TCP port. +fn free_port() -> Option { + let listener = std::net::TcpListener::bind("127.0.0.1:0").ok()?; + let port = listener.local_addr().ok()?.port(); + drop(listener); + Some(port) +} + +const ADMIN_TOKEN: &str = "test-admin-token-do-not-use-in-prod"; + +/// Every mutating control-plane route must reject an unauthenticated request +/// with 401. We exercise a representative set spanning projects, specs, +/// plugins, artifacts, compilations, and api-keys. +#[tokio::test] +async fn mutating_routes_require_admin_token() { + // EXPECTED TO FAIL until BARB-SEC-001 is fixed (require_admin not yet wired + // into create_router; mutating routes are currently reachable unauthenticated). + let Some(cp) = TestControlPlane::boot(Some(ADMIN_TOKEN)).await else { + eprintln!("skip: control plane unavailable (need DATABASE_URL + built barbacane-control)"); + return; + }; + + // (method, path, json-body) — one per mutating handler family. + let mutating: &[(reqwest::Method, &str, &str)] = &[ + (reqwest::Method::POST, "/projects", r#"{"name":"x"}"#), + ( + reqwest::Method::PUT, + "/projects/00000000-0000-0000-0000-000000000001", + r#"{"name":"y"}"#, + ), + ( + reqwest::Method::DELETE, + "/projects/00000000-0000-0000-0000-000000000001", + "", + ), + ( + reqwest::Method::POST, + "/specs", + r#"{"name":"x","content":"openapi: 3.1.0"}"#, + ), + ( + reqwest::Method::DELETE, + "/specs/00000000-0000-0000-0000-000000000002", + "", + ), + ( + reqwest::Method::POST, + "/specs/00000000-0000-0000-0000-000000000002/compile", + "{}", + ), + ( + reqwest::Method::POST, + "/plugins", + r#"{"name":"x","version":"0.1.0"}"#, + ), + (reqwest::Method::DELETE, "/plugins/x/0.1.0", ""), + ( + reqwest::Method::DELETE, + "/artifacts/00000000-0000-0000-0000-000000000003", + "", + ), + ( + reqwest::Method::DELETE, + "/compilations/00000000-0000-0000-0000-000000000004", + "", + ), + ( + reqwest::Method::POST, + "/projects/00000000-0000-0000-0000-000000000001/api-keys", + "{}", + ), + ( + reqwest::Method::POST, + "/projects/00000000-0000-0000-0000-000000000001/deploy", + "{}", + ), + ]; + + for (method, path, body) in mutating { + let resp = cp + .client + .request(method.clone(), cp.url(path)) + .header("content-type", "application/json") + .body(body.to_string()) + .send() + .await + .expect("request to control plane failed"); + assert_eq!( + resp.status(), + 401, + "{} {} must require an admin token (got {})", + method, + path, + resp.status() + ); + } +} + +/// With a valid admin token, a representative mutating route is accepted +/// (i.e. the auth layer is gating, not blanket-denying). We assert the status +/// is anything other than 401 — the request reaches the handler. +#[tokio::test] +async fn valid_admin_token_is_accepted() { + // EXPECTED TO FAIL until BARB-SEC-001 is fixed. + let Some(cp) = TestControlPlane::boot(Some(ADMIN_TOKEN)).await else { + eprintln!("skip: control plane unavailable (need DATABASE_URL + built barbacane-control)"); + return; + }; + + let resp = cp + .client + .post(cp.url("/projects")) + .header("content-type", "application/json") + .header("Authorization", format!("Bearer {}", ADMIN_TOKEN)) + .body(r#"{"name":"authz-smoke-test"}"#.to_string()) + .send() + .await + .expect("request failed"); + + assert_ne!( + resp.status(), + 401, + "a valid admin token must not be rejected as unauthorized" + ); +} + +/// `/health` must remain reachable WITHOUT a token (it is the liveness probe +/// and is explicitly excluded from the admin-auth requirement). +#[tokio::test] +async fn health_is_exempt_from_auth() { + // This is a positive control: it should pass both before and after the fix. + let Some(cp) = TestControlPlane::boot(Some(ADMIN_TOKEN)).await else { + eprintln!("skip: control plane unavailable (need DATABASE_URL + built barbacane-control)"); + return; + }; + + let resp = cp + .client + .get(cp.url("/health")) + .send() + .await + .expect("request failed"); + assert!( + resp.status().is_success(), + "/health must be reachable without an admin token" + ); +} + +/// The server must fail-closed: starting `serve` with no +/// `BARBACANE_CONTROL_ADMIN_TOKEN` set must refuse to start (so an operator +/// cannot accidentally expose an unauthenticated control plane). +#[tokio::test] +async fn server_refuses_to_start_without_admin_token() { + // EXPECTED TO FAIL until BARB-SEC-001 is fixed (serve currently starts with + // no token configured). + // + // We assert via `boot(None)` returning None *because the process exited*. + // To distinguish "exited (good)" from "no DB / no binary (skip)", we first + // require the prerequisites to exist, then assert boot fails. + if std::env::var("DATABASE_URL").is_err() || find_control_binary().is_none() { + eprintln!("skip: control plane unavailable (need DATABASE_URL + built barbacane-control)"); + return; + } + + let booted = TestControlPlane::boot(None).await; + assert!( + booted.is_none(), + "control plane must fail-closed and refuse to serve without BARBACANE_CONTROL_ADMIN_TOKEN" + ); +} + +/// Project-scoped IDOR: a credential scoped to project A must not be able to +/// read project B's spec / artifact / api-keys. +/// +/// NOTE: per-project credentials do not exist yet in the model (only a single +/// shared admin token is planned for the first hardening pass), and the global +/// `/specs/{id}` / `/artifacts/{id}` routes carry no project scoping at all. +/// This test documents the intended end-state and is parked behind `#[ignore]` +/// until the ownership model lands. +#[tokio::test] +#[ignore = "BLOCKED: per-project credentials + ownership checks on global /specs/{id} and /artifacts/{id} not implemented (BARB-SEC-001 phase 2)"] +async fn project_scoped_idor_is_blocked() { + // EXPECTED TO FAIL until BARB-SEC-001 (phase 2) is fixed. + let Some(cp) = TestControlPlane::boot(Some(ADMIN_TOKEN)).await else { + eprintln!("skip: control plane unavailable"); + return; + }; + + // Intent: create project A and project B, mint a project-A-scoped token, + // upload a spec under B, then assert A's token gets 403/404 reading B's + // resources. Wiring this requires the per-project token API, which is not + // yet present, hence #[ignore]. The shared-token boot above keeps the test + // compiling against the current binary surface. + let _ = cp.url("/projects"); +} diff --git a/crates/barbacane-test/tests/security/crypto_auth.rs b/crates/barbacane-test/tests/security/crypto_auth.rs new file mode 100644 index 0000000..5c2b6fc --- /dev/null +++ b/crates/barbacane-test/tests/security/crypto_auth.rs @@ -0,0 +1,218 @@ +//! BARB-SEC-005 — Crypto / auth: JWT validation and trust of forged client IPs. +//! +//! Two threat families, end-to-end through the gateway: +//! +//! * **JWT validation** — `alg:none`, expired `exp`, wrong `aud`, and a +//! tampered signature must all be rejected; a validly-signed token must be +//! accepted. (`jwt-auth` rejects `alg:none`/HMAC and enforces exp/aud +//! today, but real signature verification is not yet implemented — so the +//! "validly-signed token is accepted" assertion is RED.) +//! +//! * **Forged `X-Forwarded-For`** — a client-supplied XFF header must not let +//! a request bypass `ip-restriction` or steer `rate-limit` partitioning. +//! Today `ip-restriction::extract_client_ip` trusts the first XFF value +//! unconditionally, so a forged header changes the effective client IP. The +//! fix only trusts XFF from configured trusted proxies and otherwise uses +//! the real peer address. + +use barbacane_test::TestGateway; + +use crate::{encode_jwt, fixture, now_timestamp, security_fixture}; + +// --------------------------------------------------------------------------- +// JWT validation — regression locks (should pass today) + the RED signature case +// --------------------------------------------------------------------------- + +/// A token with `alg: none` must be rejected. (Regression lock: jwt-auth +/// already rejects `none`; this keeps it that way.) +#[tokio::test] +async fn jwt_alg_none_is_rejected() { + let gw = TestGateway::from_spec(&fixture("jwt-auth.yaml")) + .await + .expect("failed to start gateway"); + + let header = serde_json::json!({"alg": "none", "typ": "JWT"}); + let claims = serde_json::json!({ + "sub": "attacker", + "iss": "test-issuer", + "aud": "test-audience", + "exp": now_timestamp() + 3600, + }); + // alg:none tokens conventionally carry an empty signature. + let token = encode_jwt(&header, &claims, b""); + + let resp = gw + .request_builder(reqwest::Method::GET, "/protected") + .header("Authorization", format!("Bearer {}", token)) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 401, "alg:none must be rejected"); +} + +/// An expired token must be rejected. (Regression lock.) +#[tokio::test] +async fn jwt_expired_is_rejected() { + let gw = TestGateway::from_spec(&fixture("jwt-auth.yaml")) + .await + .expect("failed to start gateway"); + + let header = serde_json::json!({"alg": "RS256", "typ": "JWT"}); + let claims = serde_json::json!({ + "sub": "user", + "iss": "test-issuer", + "aud": "test-audience", + "exp": now_timestamp() - 120, // 2 min ago, beyond 60s skew + }); + let token = encode_jwt(&header, &claims, b"sig"); + + let resp = gw + .request_builder(reqwest::Method::GET, "/protected") + .header("Authorization", format!("Bearer {}", token)) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 401, "expired exp must be rejected"); +} + +/// A token with the wrong audience must be rejected. (Regression lock.) +#[tokio::test] +async fn jwt_wrong_audience_is_rejected() { + let gw = TestGateway::from_spec(&fixture("jwt-auth.yaml")) + .await + .expect("failed to start gateway"); + + let header = serde_json::json!({"alg": "RS256", "typ": "JWT"}); + let claims = serde_json::json!({ + "sub": "user", + "iss": "test-issuer", + "aud": "WRONG-audience", + "exp": now_timestamp() + 3600, + }); + let token = encode_jwt(&header, &claims, b"sig"); + + let resp = gw + .request_builder(reqwest::Method::GET, "/protected") + .header("Authorization", format!("Bearer {}", token)) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 401, "wrong aud must be rejected"); +} + +/// A token with a tampered signature must be rejected when signature validation +/// is enforced. (This already holds since jwt-auth fails closed when it cannot +/// verify — but it must KEEP holding once real verification lands.) +#[tokio::test] +async fn jwt_tampered_signature_is_rejected() { + let gw = TestGateway::from_spec(&security_fixture("jwt-verify.yaml")) + .await + .expect("failed to start gateway"); + + let header = serde_json::json!({"alg": "RS256", "typ": "JWT"}); + let claims = serde_json::json!({ + "sub": "user", + "iss": "test-issuer", + "aud": "test-audience", + "exp": now_timestamp() + 3600, + }); + let token = encode_jwt(&header, &claims, b"this-is-not-a-valid-signature"); + + let resp = gw + .request_builder(reqwest::Method::GET, "/protected") + .header("Authorization", format!("Bearer {}", token)) + .send() + .await + .unwrap(); + assert_eq!( + resp.status(), + 401, + "a token with an invalid signature must be rejected" + ); +} + +/// A validly-signed token must be ACCEPTED when signature validation is enforced. +/// +/// This is the discriminating case: today jwt-auth has no real signature +/// verification (it fails closed for every token when `skip_signature_validation` +/// is false), so even a correctly-signed token is rejected. The BARB-SEC-005 fix +/// implements verification against `public_key_pem`, after which a properly +/// signed token is accepted. +#[tokio::test] +#[ignore = "BLOCKED: requires real RS256 signature verification (public_key_pem) in jwt-auth, plus a matching private key in the fixture to sign with. See BARB-SEC-005."] +async fn jwt_valid_signature_is_accepted() { + // EXPECTED TO FAIL until BARB-SEC-005 is fixed (signature verification + // unimplemented; fixture also needs a real RSA keypair so the test can sign). + let gw = TestGateway::from_spec(&security_fixture("jwt-verify.yaml")) + .await + .expect("failed to start gateway"); + + // Once verification exists, sign these claims with the private key matching + // the fixture's public_key_pem and assert 200. + let _ = &gw; +} + +// --------------------------------------------------------------------------- +// Forged X-Forwarded-For +// --------------------------------------------------------------------------- + +/// A forged `X-Forwarded-For` must not let a request masquerade as a +/// denylisted IP — and, by the same token, must not let an external client +/// masquerade as an allowlisted one. The real test client is 127.0.0.1, which +/// `ip-restriction.yaml`'s `/denylist` (deny 10.0.0.0/8, 192.168.0.0/16) allows. +/// Forging `X-Forwarded-For: 10.0.0.5` currently flips the effective client IP +/// into the denied range (→ 403). With the fix, untrusted XFF is ignored and the +/// real peer (127.0.0.1) is used (→ 200). +#[tokio::test] +async fn forged_xff_does_not_bypass_ip_restriction() { + // EXPECTED TO FAIL until BARB-SEC-005 is fixed (ip-restriction trusts + // client-supplied X-Forwarded-For unconditionally). + let gw = TestGateway::from_spec(&fixture("ip-restriction.yaml")) + .await + .expect("failed to start gateway"); + + let resp = gw + .request_builder(reqwest::Method::GET, "/denylist") + .header("X-Forwarded-For", "10.0.0.5") + .send() + .await + .unwrap(); + + assert_eq!( + resp.status(), + 200, + "a forged X-Forwarded-For must be ignored; the real peer (127.0.0.1) is \ + not denylisted so the request must succeed" + ); +} + +/// A forged `X-Forwarded-For` must not partition the rate-limit buckets — an +/// attacker must not be able to dodge their own rate limit by rotating the XFF +/// header. With `partition_key: client_ip` and untrusted XFF ignored, all the +/// requests below share one bucket and the (n+1)th is limited regardless of XFF. +#[tokio::test] +async fn forged_xff_does_not_reset_rate_limit_bucket() { + // EXPECTED TO FAIL until BARB-SEC-005 is fixed (client IP, and thus the + // rate-limit partition key, is derived from spoofable XFF). + let gw = TestGateway::from_spec(&security_fixture("xff-rate-limit.yaml")) + .await + .expect("failed to start gateway"); + + // Exhaust the quota (3) while rotating XFF on every request. + let mut last_status = 0u16; + for i in 0..5 { + let resp = gw + .request_builder(reqwest::Method::GET, "/limited") + .header("X-Forwarded-For", format!("203.0.113.{}", i)) + .send() + .await + .unwrap(); + last_status = resp.status().as_u16(); + } + + assert_eq!( + last_status, 429, + "rotating X-Forwarded-For must not create fresh rate-limit buckets; \ + the 4th+ request must still be limited" + ); +} diff --git a/crates/barbacane-test/tests/security/dos.rs b/crates/barbacane-test/tests/security/dos.rs new file mode 100644 index 0000000..3e1889c --- /dev/null +++ b/crates/barbacane-test/tests/security/dos.rs @@ -0,0 +1,139 @@ +//! BARB-SEC-003 — Denial of service / resource limits. +//! +//! Three sub-areas: +//! +//! 1. **Oversized chunked body** — a `Transfer-Encoding: chunked` request with +//! an over-limit body must be rejected with 413, and the gateway must not +//! buffer the whole body before deciding (it should bail as soon as the +//! streamed size crosses the cap). +//! +//! 2. **Slowloris / missing read timeout** — documented; hard to assert +//! deterministically without flaky wall-clock timing. See the `#[ignore]` +//! test below for the intended shape. +//! +//! 3. **404-flood metric cardinality** — a flood of unique unmatched paths must +//! NOT create one Prometheus series per raw path. The router currently +//! records `record_request_metrics(&method, &path, ...)` with the *raw* +//! request path on the `RouteMatch::NotFound` arm +//! (`crates/barbacane/src/main.rs`), so an attacker can blow up metric +//! cardinality (memory DoS) and exfiltrate scanned paths. The fix replaces +//! the label with a sentinel such as ``. + +use barbacane_test::TestGateway; + +use crate::{fixture, security_fixture}; + +// --------------------------------------------------------------------------- +// 1. Oversized chunked body -> 413 +// --------------------------------------------------------------------------- + +/// A chunked request whose body exceeds the configured limit must get 413. +/// +/// `request-size-limit.yaml` caps `/limited` at 100 bytes. We send ~64 KiB with +/// `Transfer-Encoding: chunked` (reqwest streams a body of unknown length as +/// chunked). The hardened gateway rejects it with 413. +#[tokio::test] +async fn oversized_chunked_body_rejected_with_413() { + // EXPECTED TO FAIL until BARB-SEC-003 is fixed (chunked bodies without a + // Content-Length must be size-capped while streaming, not buffered then + // checked / accepted). + let gw = TestGateway::from_spec(&fixture("request-size-limit.yaml")) + .await + .expect("failed to start gateway"); + + // A 64 KiB payload (limit is 100 bytes). Using a streaming body forces + // chunked transfer-encoding (no Content-Length). + let big = vec![b'a'; 64 * 1024]; + let body = reqwest::Body::wrap_stream(futures_util::stream::once(async move { + Ok::<_, std::io::Error>(big) + })); + + let resp = gw + .request_builder(reqwest::Method::POST, "/limited") + .header("content-type", "application/json") + .body(body) + .send() + .await + .expect("request failed"); + + assert_eq!( + resp.status(), + 413, + "oversized chunked body must be rejected with 413 Payload Too Large" + ); +} + +// --------------------------------------------------------------------------- +// 2. Slowloris / missing timeout (documented, non-deterministic) +// --------------------------------------------------------------------------- + +/// Slowloris: a client that opens a connection and dribbles bytes (or never +/// finishes the body) must be timed out so it cannot pin a worker indefinitely. +/// +/// This is inherently timing-dependent and would require holding a socket open +/// and measuring that the server closes it within a header/body read deadline. +/// Asserting that deterministically (without sleeps that make CI flaky) needs a +/// configurable, observable read timeout we can drive from the test. Parked +/// until the gateway exposes a read/header timeout knob we can set low and a +/// signal we can assert on. +#[tokio::test] +#[ignore = "BLOCKED: needs a configurable+observable read/header timeout to assert slowloris defence without flaky wall-clock sleeps (BARB-SEC-003)"] +async fn slowloris_connection_is_timed_out() { + // EXPECTED TO FAIL until BARB-SEC-003 (slowloris) is addressed. + // + // Intended shape: open a raw TCP socket to the gateway, send a partial + // request ("GET /health HTTP/1.1\r\nHost: x\r\n") and then stop, and assert + // the server closes the connection within N seconds rather than waiting + // forever. Requires a deterministic timeout to assert against. +} + +// --------------------------------------------------------------------------- +// 3. 404-flood metric cardinality +// --------------------------------------------------------------------------- + +/// Flooding unique unmatched paths must not create a distinct Prometheus series +/// per raw path. We hit many random 404 paths, then scrape `/metrics` and +/// assert (a) none of the raw flood paths appear as label values, and (b) a +/// `` sentinel label is present instead. +#[tokio::test] +async fn not_found_flood_does_not_explode_metric_cardinality() { + // EXPECTED TO FAIL until BARB-SEC-003 is fixed (RouteMatch::NotFound records + // the raw request path as a metric label; should use a `` + // sentinel). + let gw = TestGateway::from_spec(&security_fixture("metrics.yaml")) + .await + .expect("failed to start gateway"); + + // Flood unique 404 paths. + let unique_paths: Vec = (0..50) + .map(|i| format!("/nonexistent-scan-{}-{}", std::process::id(), i)) + .collect(); + for p in &unique_paths { + let resp = gw.get(p).await.unwrap(); + assert_eq!(resp.status(), 404, "{} should be a 404", p); + } + + // Scrape the Prometheus metrics from the admin port. + let metrics = gw + .admin_get("/metrics") + .await + .unwrap() + .text() + .await + .unwrap(); + + // (a) No raw flood path may appear as a metric label value. + for p in &unique_paths { + assert!( + !metrics.contains(p.as_str()), + "raw 404 path {} leaked into Prometheus labels (unbounded cardinality DoS)", + p + ); + } + + // (b) Unmatched requests should collapse to a sentinel label. + assert!( + metrics.contains(""), + "expected a `` sentinel label for unmatched requests" + ); +} diff --git a/crates/barbacane-test/tests/security/sandbox.rs b/crates/barbacane-test/tests/security/sandbox.rs new file mode 100644 index 0000000..99be890 --- /dev/null +++ b/crates/barbacane-test/tests/security/sandbox.rs @@ -0,0 +1,63 @@ +//! BARB-SEC-004 — Plugin sandbox / capability confinement. +//! +//! Threat: the WASM linker registers ALL host functions +//! (`add_host_functions` in `crates/barbacane-wasm/src/instance.rs`) regardless +//! of what a plugin's manifest grants. A plugin whose manifest declares only the +//! `log` capability can therefore still *import and call* `host_get_secret`, +//! `host_http_call`, etc. — capability declarations are advisory at link time. +//! +//! Today the only enforcement is `validate_imports` +//! (`crates/barbacane-wasm/src/validate.rs`), which rejects a module whose +//! imports are not covered by its declared capabilities. That is load-time +//! import validation, not a per-plugin linker. The hardening goal is a +//! per-plugin, default-deny linker: a plugin only gets host functions for the +//! capabilities it was granted, so even a module that smuggles an import past +//! validation has nothing to call. +//! +//! ## Why this is expressed but `#[ignore]`d +//! +//! Asserting default-deny end-to-end requires either: +//! * a purpose-built adversarial WASM plugin that declares `log` only but +//! attempts `host_http_call` / `host_get_secret` (not present in the repo, +//! and building wasm in the integration harness is out of scope), or +//! * driving `barbacane-wasm` internals directly — but `barbacane-test` does +//! not (and per the additive constraint must not be made to) depend on +//! `barbacane-wasm`, and the linker entry points are private. +//! +//! So we document the intent and leave the test `#[ignore]`d with a precise +//! note on what the maintainer must expose / build. The `validate_imports` +//! positive control below *can* run today if exposed; it is also gated because +//! the crate dependency is missing. + +/// A plugin granted only `log` must not be able to reach any other host +/// function. With a per-plugin default-deny linker, an adversarial module that +/// declares `capabilities = [log]` but imports `host_http_call` must fail to +/// instantiate (or the call must trap), never silently succeed. +#[tokio::test] +#[ignore = "BLOCKED: needs (a) an adversarial fixture plugin declaring only `log` but importing host_http_call, and (b) a per-plugin default-deny linker in barbacane-wasm (currently add_host_functions registers ALL host fns). See BARB-SEC-004."] +async fn log_only_plugin_cannot_call_other_host_functions() { + // EXPECTED TO FAIL until BARB-SEC-004 is fixed. + // + // Intended shape (once the adversarial plugin + per-plugin linker exist): + // 1. Compile a spec that bundles the `log-only-but-calls-http` plugin. + // 2. Boot the gateway; route a request through that plugin. + // 3. Assert the host call is denied — the plugin's attempt to invoke + // host_http_call must trap/error, not perform the outbound request. +} + +/// Load-time positive control: a module whose imports exceed its declared +/// capabilities must be rejected by `validate_imports`. +#[tokio::test] +#[ignore = "BLOCKED: barbacane-test does not depend on barbacane-wasm; to run this, the maintainer must add barbacane-wasm as a dev-dependency and keep validate::validate_imports public. See BARB-SEC-004."] +async fn imports_exceeding_declared_capabilities_are_rejected() { + // EXPECTED TO FAIL TO COMPILE without the barbacane-wasm dev-dependency. + // + // Intended body: + // let module = Module::new(&engine, adversarial_wasm)?; + // let result = barbacane_wasm::validate::validate_imports( + // &module, + // &["log".to_string()], // declares only `log` + // ); + // assert!(result.is_err(), "module importing host_http_call under a + // log-only manifest must be rejected"); +} diff --git a/crates/barbacane-test/tests/security/ssrf.rs b/crates/barbacane-test/tests/security/ssrf.rs new file mode 100644 index 0000000..d89b00a --- /dev/null +++ b/crates/barbacane-test/tests/security/ssrf.rs @@ -0,0 +1,100 @@ +//! BARB-SEC-002 — Server-Side Request Forgery via the WASM host HTTP client. +//! +//! Threat: a plugin (or plugin *config*, which a spec author or a compromised +//! control plane controls) can make the host issue outbound HTTP to an +//! arbitrary URL. Without an egress policy, a plugin can reach the cloud +//! instance-metadata service (169.254.169.254), loopback services, or internal +//! RFC1918 hosts — classic SSRF. +//! +//! The fix: the host HTTP client +//! (`crates/barbacane-wasm/src/http_client.rs`) must resolve the target host +//! and refuse to connect when the resolved IP is loopback / link-local / +//! private / metadata, INCLUDING after following redirects (so a public URL +//! that 30x-redirects to 169.254.169.254 is still blocked). +//! +//! ## How these tests observe SSRF +//! +//! We drive the `opa-authz` middleware, whose `opa_url` is a plugin-controlled +//! outbound call. The fixture mock dispatcher returns HTTP 200 only if the +//! middleware *allows* the request, which only happens if the host call +//! succeeds and the (non-existent) OPA endpoint returns an allow decision. +//! +//! The security invariant is therefore: **a request whose `opa_url` points at a +//! blocked address must never return 200** (the protected upstream must never be +//! reached). Today the host does not enforce an egress denylist, so a request +//! pointed at, e.g., a co-located metadata mock could succeed — hence RED. Once +//! the denylist lands, the host refuses the connection and the middleware +//! fails closed (403/503), which is what we assert. + +use barbacane_test::TestGateway; + +use crate::security_fixture; + +/// Boot a gateway from the SSRF fixture. +async fn ssrf_gateway() -> TestGateway { + TestGateway::from_spec(&security_fixture("ssrf.yaml")) + .await + .expect("ssrf fixture failed to compile/start") +} + +/// The protected response (200 "reached") must never be returned for an +/// `opa_url` pointing at the instance-metadata service. +#[tokio::test] +async fn metadata_endpoint_is_blocked() { + // EXPECTED TO FAIL until BARB-SEC-002 is fixed (host HTTP client has no + // resolved-IP denylist for link-local / metadata addresses). + let gw = ssrf_gateway().await; + let resp = gw.get("/ssrf-metadata").await.unwrap(); + assert_ne!( + resp.status(), + 200, + "host must refuse outbound HTTP to 169.254.169.254 (metadata SSRF); \ + the protected upstream must never be reached" + ); +} + +/// Loopback targets must be blocked. +#[tokio::test] +async fn loopback_is_blocked() { + // EXPECTED TO FAIL until BARB-SEC-002 is fixed. + let gw = ssrf_gateway().await; + let resp = gw.get("/ssrf-loopback").await.unwrap(); + assert_ne!( + resp.status(), + 200, + "host must refuse outbound HTTP to 127.0.0.0/8 (loopback SSRF)" + ); +} + +/// RFC1918 private addresses must be blocked. +#[tokio::test] +async fn private_rfc1918_is_blocked() { + // EXPECTED TO FAIL until BARB-SEC-002 is fixed. + let gw = ssrf_gateway().await; + let resp = gw.get("/ssrf-private").await.unwrap(); + assert_ne!( + resp.status(), + 200, + "host must refuse outbound HTTP to 10.0.0.0/8 (private-network SSRF)" + ); +} + +/// A hostname that resolves to a metadata/link-local IP must be blocked after +/// DNS resolution (the denylist must operate on the *resolved* IP, not the +/// literal string). `metadata.google.internal` resolves to 169.254.169.254 in +/// GCP; in CI it typically does not resolve, so this also covers the +/// redirect-to-metadata shape: in either case the protected upstream must not +/// be reached. +#[tokio::test] +async fn dns_resolved_metadata_is_blocked() { + // EXPECTED TO FAIL until BARB-SEC-002 is fixed (resolve-then-check, incl. + // redirect targets). + let gw = ssrf_gateway().await; + let resp = gw.get("/ssrf-dns-metadata").await.unwrap(); + assert_ne!( + resp.status(), + 200, + "host must resolve the hostname and refuse link-local / metadata IPs, \ + including via redirects" + ); +} diff --git a/crates/barbacane-wasm/src/chain.rs b/crates/barbacane-wasm/src/chain.rs index fe267db..5f3bbd0 100644 --- a/crates/barbacane-wasm/src/chain.rs +++ b/crates/barbacane-wasm/src/chain.rs @@ -347,10 +347,14 @@ pub fn parse_middleware_output( let data = serde_json::to_vec(&parsed.data) .map_err(|e| WasmError::InitFailed(format!("failed to serialize output: {}", e)))?; - if parsed.action == 0 || result_code == 0 { - Ok(OnRequestResult::Continue(data)) - } else { + // Fail safe: short-circuit if EITHER the structured action or the + // ABI result code signals it. Continue only when both agree, so a + // deny-intent middleware (action != 0) is never downgraded to + // Continue just because its return code happened to be 0. + if parsed.action != 0 || result_code != 0 { Ok(OnRequestResult::ShortCircuit(data)) + } else { + Ok(OnRequestResult::Continue(data)) } } Err(_) => { @@ -429,6 +433,20 @@ mod tests { assert!(matches!(result, OnRequestResult::ShortCircuit(_))); } + #[test] + fn parse_deny_action_not_downgraded_when_result_code_zero() { + // WA-10 regression: a deny-intent middleware (action != 0) must + // short-circuit even if its ABI return code is 0 — never fail open. + let output = serde_json::to_vec(&json!({ + "action": 1, + "data": {"status": 403, "body": "Forbidden"} + })) + .unwrap(); + + let result = parse_middleware_output(&output, 0).unwrap(); + assert!(matches!(result, OnRequestResult::ShortCircuit(_))); + } + #[test] fn parse_raw_output_continue() { let output = b"raw request data"; diff --git a/crates/barbacane-wasm/src/http_client.rs b/crates/barbacane-wasm/src/http_client.rs index 19c0b0e..a8b66d1 100644 --- a/crates/barbacane-wasm/src/http_client.rs +++ b/crates/barbacane-wasm/src/http_client.rs @@ -4,6 +4,7 @@ use std::collections::HashMap; use std::hash::{Hash, Hasher}; +use std::net::IpAddr; use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; @@ -112,6 +113,9 @@ impl HttpClient { .pool_idle_timeout(config.pool_idle_timeout) .connect_timeout(config.connect_timeout) .timeout(config.default_timeout) + // Disable redirect following: a permitted host could otherwise 3xx + // to an internal/metadata target, bypassing the SSRF guard below. + .redirect(reqwest::redirect::Policy::none()) .build() .map_err(HttpClientError::BuildError)?; @@ -160,7 +164,8 @@ impl HttpClient { .pool_max_idle_per_host(self.base_config.pool_max_idle_per_host) .pool_idle_timeout(self.base_config.pool_idle_timeout) .connect_timeout(self.base_config.connect_timeout) - .timeout(self.base_config.default_timeout); + .timeout(self.base_config.default_timeout) + .redirect(reqwest::redirect::Policy::none()); // Add client certificate (mTLS) if let (Some(cert_path), Some(key_path)) = (&tls_config.client_cert, &tls_config.client_key) @@ -225,6 +230,9 @@ impl HttpClient { .ok_or_else(|| HttpClientError::InvalidUrl("missing host".into()))? .to_string(); + // SSRF guard: reject internal/loopback/link-local/metadata targets. + ssrf_guard(&url, self.base_config.allow_internal_egress).await?; + let circuit_state = self.get_circuit_state(&host); if circuit_state == crate::circuit_breaker::CircuitState::Open { return Err(HttpClientError::CircuitOpen(host)); @@ -284,6 +292,9 @@ impl HttpClient { .ok_or_else(|| HttpClientError::InvalidUrl("missing host".into()))? .to_string(); + // SSRF guard: reject internal/loopback/link-local/metadata targets. + ssrf_guard(&url, self.base_config.allow_internal_egress).await?; + // Check circuit breaker let circuit_state = self.get_circuit_state(&host); if circuit_state == CircuitState::Open { @@ -390,6 +401,84 @@ impl HttpClient { } } +/// Whether an IP address points at an internal / non-routable / metadata range +/// that an untrusted plugin must not be able to reach. +fn ip_is_internal(ip: &IpAddr) -> bool { + match ip { + IpAddr::V4(v4) => { + v4.is_loopback() + || v4.is_private() + || v4.is_link_local() // 169.254.0.0/16, incl. 169.254.169.254 cloud metadata + || v4.is_unspecified() + || v4.is_broadcast() + || v4.is_multicast() + || v4.octets()[0] == 0 // 0.0.0.0/8 + || (v4.octets()[0] == 100 && (v4.octets()[1] & 0xC0) == 64) // 100.64.0.0/10 CGNAT + } + IpAddr::V6(v6) => { + // Unwrap IPv4-in-IPv6 forms and apply the v4 rules. + if let Some(mapped) = v6.to_ipv4_mapped() { + return ip_is_internal(&IpAddr::V4(mapped)); + } + if let Some(compat) = v6.to_ipv4() { + return ip_is_internal(&IpAddr::V4(compat)); + } + let seg = v6.segments(); + v6.is_loopback() + || v6.is_unspecified() + || v6.is_multicast() + || (seg[0] & 0xfe00) == 0xfc00 // fc00::/7 unique-local + || (seg[0] & 0xffc0) == 0xfe80 // fe80::/10 link-local + } + } +} + +/// Reject requests whose target resolves to an internal address. Hostnames are +/// resolved and rejected if *any* resolved address is internal, defending +/// against a public name that points at an internal IP. +/// +/// Note: a separate connect-time resolution still occurs inside reqwest, so a +/// rebinding attacker could in theory return a different address; pinning the +/// vetted IP via a custom resolver is tracked as follow-up hardening. +async fn ssrf_guard(url: &reqwest::Url, allow_internal: bool) -> Result<(), HttpClientError> { + if allow_internal { + return Ok(()); + } + + let host = url + .host_str() + .ok_or_else(|| HttpClientError::InvalidUrl("missing host".into()))?; + let host_clean = host + .strip_prefix('[') + .and_then(|h| h.strip_suffix(']')) + .unwrap_or(host); + + if let Ok(ip) = host_clean.parse::() { + if ip_is_internal(&ip) { + return Err(HttpClientError::BlockedTarget(host.to_string())); + } + return Ok(()); + } + + let port = url.port_or_known_default().unwrap_or(0); + let mut saw_any = false; + let addrs = tokio::net::lookup_host((host_clean, port)) + .await + .map_err(|e| HttpClientError::ConnectionFailed(e.to_string()))?; + for addr in addrs { + saw_any = true; + if ip_is_internal(&addr.ip()) { + return Err(HttpClientError::BlockedTarget(host.to_string())); + } + } + if !saw_any { + return Err(HttpClientError::ConnectionFailed(format!( + "no DNS records for {host}" + ))); + } + Ok(()) +} + /// Configuration for the HTTP client. #[derive(Debug, Clone)] pub struct HttpClientConfig { @@ -403,6 +492,10 @@ pub struct HttpClientConfig { pub default_timeout: Duration, /// Allow plaintext HTTP (development only). pub allow_plaintext: bool, + /// Allow plugin egress to internal/loopback/link-local/metadata targets, + /// disabling the SSRF guard. Off by default; operators opt in for trusted + /// internal upstreams. + pub allow_internal_egress: bool, } impl Default for HttpClientConfig { @@ -413,6 +506,7 @@ impl Default for HttpClientConfig { connect_timeout: Duration::from_secs(10), default_timeout: Duration::from_secs(30), allow_plaintext: false, + allow_internal_egress: false, } } } @@ -489,6 +583,9 @@ pub enum HttpClientError { #[error("circuit breaker open for host: {0}")] CircuitOpen(String), + #[error("target blocked by SSRF policy: {0}")] + BlockedTarget(String), + #[error("request timeout")] Timeout, @@ -533,6 +630,59 @@ mod option_duration_serde { mod tests { use super::*; + #[test] + fn ssrf_classifier_blocks_internal_targets() { + let blocked = [ + "127.0.0.1", + "169.254.169.254", // cloud metadata + "10.0.0.5", + "192.168.1.1", + "172.16.0.1", + "0.0.0.0", + "100.64.0.1", // CGNAT + "::1", // IPv6 loopback + "fc00::1", // IPv6 ULA + "fe80::1", // IPv6 link-local + "::ffff:127.0.0.1", // IPv4-mapped loopback + "::ffff:169.254.169.254", + ]; + for s in blocked { + let ip: IpAddr = s.parse().unwrap(); + assert!(ip_is_internal(&ip), "{s} should be classified internal"); + } + + let allowed = [ + "8.8.8.8", + "1.1.1.1", + "93.184.216.34", + "2606:4700:4700::1111", + ]; + for s in allowed { + let ip: IpAddr = s.parse().unwrap(); + assert!(!ip_is_internal(&ip), "{s} should be classified external"); + } + } + + #[tokio::test] + async fn ssrf_guard_rejects_ip_literals_to_metadata_and_loopback() { + for url in [ + "http://169.254.169.254/latest/meta-data/", + "http://127.0.0.1:8081/", + "https://[::1]/", + "http://10.1.2.3/", + ] { + let parsed = url.parse::().unwrap(); + let err = ssrf_guard(&parsed, false).await.unwrap_err(); + assert!( + matches!(err, HttpClientError::BlockedTarget(_)), + "{url} should be blocked, got {err:?}" + ); + } + // With internal egress allowed, the same targets pass the guard. + let parsed = "http://127.0.0.1:8081/".parse::().unwrap(); + assert!(ssrf_guard(&parsed, true).await.is_ok()); + } + #[test] fn test_config_default() { let config = HttpClientConfig::default(); @@ -693,6 +843,7 @@ mod tests { async fn stream_raw_rejects_invalid_method() { let config = HttpClientConfig { allow_plaintext: true, + allow_internal_egress: true, ..Default::default() }; let client = HttpClient::new(config).expect("client"); @@ -713,6 +864,7 @@ mod tests { async fn stream_raw_connection_refused() { let config = HttpClientConfig { allow_plaintext: true, + allow_internal_egress: true, ..Default::default() }; let client = HttpClient::new(config).expect("client"); @@ -743,6 +895,7 @@ mod tests { let config = HttpClientConfig { allow_plaintext: true, + allow_internal_egress: true, ..Default::default() }; let client = HttpClient::new(config).expect("client"); @@ -782,6 +935,7 @@ mod tests { let config = HttpClientConfig { allow_plaintext: true, + allow_internal_egress: true, ..Default::default() }; let client = HttpClient::new(config).expect("client"); diff --git a/crates/barbacane-wasm/src/manifest.rs b/crates/barbacane-wasm/src/manifest.rs index 9c4a086..d6c8b44 100644 --- a/crates/barbacane-wasm/src/manifest.rs +++ b/crates/barbacane-wasm/src/manifest.rs @@ -154,6 +154,8 @@ const KNOWN_CAPABILITIES: &[&str] = &[ "generate_uuid", "verify_signature", "ws_upgrade", + "cache", + "rate_limit", ]; /// Check if a capability name is known. @@ -167,11 +169,23 @@ pub fn capability_to_imports(capability: &str) -> &'static [&'static str] { "log" => &["host_log"], "context_get" => &["host_context_get", "host_context_read_result"], "context_set" => &["host_context_set"], - "clock_now" => &["host_clock_now"], + // clock_now: canonical + the time aliases the host still exposes. + "clock_now" => &["host_clock_now", "host_time_now", "host_get_unix_timestamp"], "get_secret" => &["host_get_secret", "host_secret_read_result"], - "http_call" => &["host_http_call", "host_http_read_result"], - "kafka_publish" => &["host_kafka_publish"], - "nats_publish" => &["host_nats_publish"], + // http_call also covers the outbound-body side-channel functions. + "http_call" => &[ + "host_http_call", + "host_http_read_result", + "host_http_stream", + "host_http_request_body_set", + "host_http_response_body_len", + "host_http_response_body_read", + ], + // Broker dispatchers read async results via the shared broker channel. + "kafka_publish" => &["host_kafka_publish", "host_broker_read_result"], + "nats_publish" => &["host_nats_publish", "host_broker_read_result"], + "cache" => &["host_cache_get", "host_cache_set", "host_cache_read_result"], + "rate_limit" => &["host_rate_limit_check", "host_rate_limit_read_result"], "telemetry" => &[ "host_metric_counter_inc", "host_metric_histogram_observe", diff --git a/crates/barbacane-wasm/src/secrets.rs b/crates/barbacane-wasm/src/secrets.rs index 602316c..954c53e 100644 --- a/crates/barbacane-wasm/src/secrets.rs +++ b/crates/barbacane-wasm/src/secrets.rs @@ -64,6 +64,46 @@ impl SecretsStore { } } +/// Confine a `file://` secret path to the operator-configured secrets +/// directory (`BARBACANE_SECRETS_DIR`). +/// +/// Without this, `file://` is an arbitrary-file-read primitive +/// (`file:///etc/shadow`, `file:///proc/self/environ`). We require an explicit +/// base directory and verify, after canonicalization (which resolves symlinks +/// and `..`), that the target stays inside it. Fails closed when the base dir +/// is not configured. +fn confine_secret_path(path: &str) -> Result { + let base = std::env::var_os("BARBACANE_SECRETS_DIR").ok_or_else(|| { + SecretsError::FileReadError( + "file:// secret references require BARBACANE_SECRETS_DIR to be set".to_string(), + ) + })?; + let base = std::path::Path::new(&base) + .canonicalize() + .map_err(|e| SecretsError::FileReadError(format!("invalid BARBACANE_SECRETS_DIR: {e}")))?; + + let requested = std::path::Path::new(path); + let joined = if requested.is_absolute() { + requested.to_path_buf() + } else { + base.join(requested) + }; + let canonical = joined.canonicalize().map_err(|e| { + if e.kind() == std::io::ErrorKind::NotFound { + SecretsError::FileNotFound(path.to_string()) + } else { + SecretsError::FileReadError(format!("{}: {}", path, e)) + } + })?; + + if !canonical.starts_with(&base) { + return Err(SecretsError::FileReadError(format!( + "secret path '{path}' escapes BARBACANE_SECRETS_DIR" + ))); + } + Ok(canonical) +} + /// Check if a string value is a secret reference. pub fn is_secret_reference(value: &str) -> bool { value.starts_with("env://") @@ -77,12 +117,14 @@ pub fn is_secret_reference(value: &str) -> bool { /// /// Currently supports: /// - `env://VAR_NAME` - Environment variable -/// - `file:///path/to/secret` - File content (trimmed) +/// - `file:///path/to/secret` - File content (trimmed), confined to the +/// directory named by `BARBACANE_SECRETS_DIR` pub fn resolve_secret(reference: &str) -> Result { if let Some(var_name) = reference.strip_prefix("env://") { std::env::var(var_name).map_err(|_| SecretsError::EnvNotFound(var_name.to_string())) } else if let Some(path) = reference.strip_prefix("file://") { - std::fs::read_to_string(path) + let resolved = confine_secret_path(path)?; + std::fs::read_to_string(&resolved) .map(|s| s.trim().to_string()) .map_err(|e| { if e.kind() == std::io::ErrorKind::NotFound { @@ -235,22 +277,42 @@ mod tests { assert!(matches!(result, Err(SecretsError::EnvNotFound(_)))); } + // All `file://` assertions live in one test: they mutate the shared + // BARBACANE_SECRETS_DIR env var, so keeping them sequential avoids the + // cross-thread race that separate parallel tests would introduce. #[test] - fn test_resolve_file_secret() { + fn test_resolve_file_secret_confinement() { use std::io::Write; let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("secret.txt"); let mut file = std::fs::File::create(&path).unwrap(); writeln!(file, "file-secret-value").unwrap(); - let result = resolve_secret(&format!("file://{}", path.display())); - assert_eq!(result.unwrap(), "file-secret-value"); - } - - #[test] - fn test_resolve_file_not_found() { - let result = resolve_secret("file:///nonexistent/path/to/secret"); - assert!(matches!(result, Err(SecretsError::FileNotFound(_)))); + // Fail closed when no secrets dir is configured. + std::env::remove_var("BARBACANE_SECRETS_DIR"); + assert!(matches!( + resolve_secret(&format!("file://{}", path.display())), + Err(SecretsError::FileReadError(_)) + )); + + // Confined read succeeds inside the configured dir. + std::env::set_var("BARBACANE_SECRETS_DIR", dir.path()); + assert_eq!( + resolve_secret(&format!("file://{}", path.display())).unwrap(), + "file-secret-value" + ); + + // Absolute path outside the dir is rejected. + assert!(resolve_secret("file:///etc/hostname").is_err()); + + // Missing file inside the dir → FileNotFound. + let missing = dir.path().join("nope.txt"); + assert!(matches!( + resolve_secret(&format!("file://{}", missing.display())), + Err(SecretsError::FileNotFound(_)) + )); + + std::env::remove_var("BARBACANE_SECRETS_DIR"); } #[test] diff --git a/crates/barbacane-wasm/src/validate.rs b/crates/barbacane-wasm/src/validate.rs index 7992780..01dcdc3 100644 --- a/crates/barbacane-wasm/src/validate.rs +++ b/crates/barbacane-wasm/src/validate.rs @@ -49,8 +49,18 @@ pub fn validate_imports( // Build the set of allowed imports let mut allowed: HashSet<&str> = HashSet::new(); - // host_set_output is always allowed (not a capability) - allowed.insert("host_set_output"); + // Core ABI functions are always allowed — they are part of the plugin + // contract (output + the request/response body side-channel governed by the + // manifest `body_access` flag), not capability-gated host functions. + for core in [ + "host_set_output", + "host_body_len", + "host_body_read", + "host_body_set", + "host_body_clear", + ] { + allowed.insert(core); + } // Add imports for each declared capability for capability in declared_capabilities { @@ -210,4 +220,34 @@ mod tests { let imports = capability_to_imports("unknown"); assert!(imports.is_empty()); } + + fn module_with_import(import: &str) -> wasmtime::Module { + let engine = wasmtime::Engine::default(); + let wat = format!( + r#"(module + (import "barbacane" "{import}" (func (param i32 i32))) + (memory (export "memory") 1))"# + ); + let wasm = wat::parse_str(&wat).expect("valid wat"); + wasmtime::Module::new(&engine, &wasm).expect("valid module") + } + + #[test] + fn validate_imports_rejects_undeclared_capability() { + // A plugin that declares only `log` must not be able to import + // host_http_call (the http_call capability) — default-deny. + let module = module_with_import("host_http_call"); + let declared = vec!["log".to_string()]; + let err = validate_imports(&module, &declared).unwrap_err(); + assert!(matches!(err, WasmError::UndeclaredImport(name) if name == "host_http_call")); + } + + #[test] + fn validate_imports_allows_declared_capability_and_core_abi() { + // host_log is covered by the declared `log` capability... + assert!(validate_imports(&module_with_import("host_log"), &["log".to_string()]).is_ok()); + // ...and the core body ABI is always allowed regardless of capabilities. + assert!(validate_imports(&module_with_import("host_body_read"), &[]).is_ok()); + assert!(validate_imports(&module_with_import("host_set_output"), &[]).is_ok()); + } } diff --git a/crates/barbacane/src/admin.rs b/crates/barbacane/src/admin.rs index 2206351..3b89128 100644 --- a/crates/barbacane/src/admin.rs +++ b/crates/barbacane/src/admin.rs @@ -183,6 +183,9 @@ mod tests { source: Some("ci/github-actions".to_string()), }, mcp: barbacane_compiler::McpConfig::default(), + signature: None, + signing_public_key: None, + capabilities_enforced: false, } } @@ -279,6 +282,9 @@ mod tests { artifact_hash: "sha256:test".to_string(), provenance: barbacane_compiler::Provenance::default(), mcp: barbacane_compiler::McpConfig::default(), + signature: None, + signing_public_key: None, + capabilities_enforced: false, }; let state = Arc::new(AdminState { manifest: Arc::new(ArcSwap::new(Arc::new(manifest))), @@ -317,6 +323,9 @@ mod tests { artifact_hash: "sha256:test".to_string(), provenance: barbacane_compiler::Provenance::default(), mcp: barbacane_compiler::McpConfig::default(), + signature: None, + signing_public_key: None, + capabilities_enforced: false, }; let state = Arc::new(AdminState { manifest: Arc::new(ArcSwap::new(Arc::new(manifest))), diff --git a/crates/barbacane/src/main.rs b/crates/barbacane/src/main.rs index a470d9e..9d387b1 100644 --- a/crates/barbacane/src/main.rs +++ b/crates/barbacane/src/main.rs @@ -44,6 +44,12 @@ use uuid::Uuid; /// Server version for the Server header. const SERVER_VERSION: &str = concat!("barbacane/", env!("CARGO_PKG_VERSION")); +/// Metric `path` label used for requests that never matched a declared route +/// (unmatched, method-not-allowed, or rejected before routing). Using a fixed +/// sentinel instead of the raw request path keeps Prometheus series cardinality +/// bounded — a scanner hitting random URLs cannot create unbounded time-series. +const UNMATCHED_ROUTE_LABEL: &str = ""; + use barbacane_telemetry::MetricsRegistry; use std::collections::HashMap; @@ -688,9 +694,18 @@ impl Gateway { let specs = load_specs(artifact_path).map_err(|e| format!("failed to load specs: {}", e))?; - // Initialize HTTP client for upstream requests and plugin outbound calls + // Initialize HTTP client for upstream requests and plugin outbound calls. + // SSRF guard is on by default; operators opt out for trusted internal + // upstreams via BARBACANE_ALLOW_INTERNAL_EGRESS. + let allow_internal_egress = matches!( + std::env::var("BARBACANE_ALLOW_INTERNAL_EGRESS") + .ok() + .as_deref(), + Some("1" | "true" | "TRUE" | "yes") + ); let http_client_config = HttpClientConfig { allow_plaintext: allow_plaintext_upstream, + allow_internal_egress, ..Default::default() }; let http_client = HttpClient::new(http_client_config) @@ -718,6 +733,40 @@ impl Gateway { tracing::warn!("no plugins bundled in artifact - ensure barbacane.yaml manifest was used during compilation"); } + // AR-1: verify artifact integrity before instantiating any plugin. + // The hash/checksum checks always run (detect tampering or corruption); + // an Ed25519 signature is additionally required when a trusted public + // key is pinned via BARBACANE_TRUSTED_PUBKEY. + barbacane_compiler::verify_artifact_hash(&manifest) + .map_err(|e| format!("artifact integrity check failed: {}", e))?; + for (name, loaded) in &bundled_plugins { + barbacane_compiler::verify_plugin_checksum(&manifest, name, &loaded.wasm_bytes) + .map_err(|e| format!("artifact integrity check failed: {}", e))?; + } + match std::env::var("BARBACANE_TRUSTED_PUBKEY") { + Ok(pubkey) if !pubkey.trim().is_empty() => { + barbacane_compiler::verify_artifact_signature(&manifest, &pubkey) + .map_err(|e| format!("artifact signature verification failed: {}", e))?; + tracing::info!("artifact Ed25519 signature verified"); + } + _ => { + tracing::warn!( + "artifact signature verification disabled; set BARBACANE_TRUSTED_PUBKEY to require a valid Ed25519 signature" + ); + } + } + + // WA-1: capability enforcement only applies to artifacts whose per-plugin + // capabilities are authoritative (compiled from plugin.toml). Artifacts + // built without them (e.g. via the control-plane registry, which does + // not yet persist capabilities) are loaded without enforcement. + let enforce_capabilities = manifest.capabilities_enforced; + if !enforce_capabilities { + tracing::warn!( + "plugin capability enforcement disabled: artifact has no authoritative capabilities" + ); + } + // Compile all plugin modules first (we'll register them after creating the final pool) let mut compiled_modules = Vec::new(); for (name, loaded) in bundled_plugins { @@ -729,6 +778,26 @@ impl Gateway { loaded.body_access, ) .map_err(|e| format!("failed to compile plugin '{}': {}", name, e))?; + + // WA-1: enforce the capability contract. The plugin may only import + // host functions covered by the capabilities it declared in + // plugin.toml (carried in the artifact manifest); anything else is a + // hard load failure (default-deny). + if enforce_capabilities { + let declared = manifest + .plugins + .iter() + .find(|p| p.name == name) + .map(|p| p.capabilities.host_functions.clone()) + .unwrap_or_default(); + barbacane_wasm::validate_imports(module.module(), &declared).map_err(|e| { + format!( + "plugin '{}' violates its declared capabilities: {}", + name, e + ) + })?; + } + compiled_modules.push((name, loaded.version, module)); } @@ -897,15 +966,18 @@ impl Gateway { ) -> Response { let headers = response.headers_mut(); - // Observability headers + // Observability headers. `request_id`/`trace_id` may originate from + // client-supplied headers, so never panic on a value that isn't a legal + // header value — fall back to a placeholder instead. headers.insert("server", HeaderValue::from_static(SERVER_VERSION)); headers.insert( "x-request-id", - HeaderValue::from_str(request_id).expect("uuid is valid ASCII"), + HeaderValue::from_str(request_id) + .unwrap_or_else(|_| HeaderValue::from_static("invalid")), ); headers.insert( "x-trace-id", - HeaderValue::from_str(trace_id).expect("uuid is valid ASCII"), + HeaderValue::from_str(trace_id).unwrap_or_else(|_| HeaderValue::from_static("invalid")), ); // Security headers (enabled by default) @@ -953,11 +1025,14 @@ impl Gateway { let method = req.method().clone(); let method_str = method.as_str().to_string(); - // Generate or extract request ID (from incoming header or new UUID) + // Generate or extract request ID (from incoming header or new UUID). + // Only accept a client-supplied id if it is a legal, non-empty header + // value; otherwise generate one (prevents header-injection / panics). let request_id = req .headers() .get("x-request-id") .and_then(|v| v.to_str().ok()) + .filter(|s| !s.is_empty() && HeaderValue::from_str(s).is_ok()) .map(|s| s.to_string()) .unwrap_or_else(|| Uuid::new_v4().to_string()); @@ -982,7 +1057,8 @@ impl Gateway { let response = self.validation_error_response(&[e]); self.record_request_metrics( &method_str, - &path, + // Bounded label: raw paths would let a scanner explode metric cardinality. + UNMATCHED_ROUTE_LABEL, response.status().as_u16(), 0, 0, @@ -1025,7 +1101,7 @@ impl Gateway { let response = self.validation_error_response(&[e]); self.record_request_metrics( &method_str, - &path, + UNMATCHED_ROUTE_LABEL, response.status().as_u16(), 0, 0, @@ -1045,7 +1121,7 @@ impl Gateway { let response = self.validation_error_response(&[e]); self.record_request_metrics( &method_str, - &path, + UNMATCHED_ROUTE_LABEL, response.status().as_u16(), 0, 0, @@ -1212,7 +1288,7 @@ impl Gateway { .await; self.record_request_metrics( &method_str, - &path, + UNMATCHED_ROUTE_LABEL, response.status().as_u16(), 0, 0, @@ -1228,7 +1304,7 @@ impl Gateway { let response = self.method_not_allowed_response(allowed, &method_str, &path); self.record_request_metrics( &method_str, - &path, + UNMATCHED_ROUTE_LABEL, response.status().as_u16(), 0, 0, @@ -1244,7 +1320,7 @@ impl Gateway { let response = self.not_found_response(); self.record_request_metrics( &method_str, - &path, + UNMATCHED_ROUTE_LABEL, response.status().as_u16(), 0, 0, diff --git a/crates/barbacane/src/mcp/mod.rs b/crates/barbacane/src/mcp/mod.rs index 393dc1d..fc9423b 100644 --- a/crates/barbacane/src/mcp/mod.rs +++ b/crates/barbacane/src/mcp/mod.rs @@ -108,20 +108,22 @@ impl McpServer { let is_notification = req.id.is_none(); - // Session validation for non-initialize, non-notification requests + // Session validation for non-initialize, non-notification requests. + // Fail closed: a missing session header is rejected exactly like an + // invalid one. Otherwise a client could skip the check entirely (and + // reach `tools/list` / `tools/call`) simply by omitting `Mcp-Session-Id`. if req.method != "initialize" && !is_notification { - if let Some(sid) = session_id { - if !self.session_store.touch(sid) { - let resp = JsonRpcResponse::error( - req.id, - INVALID_REQUEST, - "invalid or expired session", - ); - return McpResult::Response { - body: serde_json::to_vec(&resp).unwrap_or_default(), - session_id: None, - }; - } + let session_ok = session_id.is_some_and(|sid| self.session_store.touch(sid)); + if !session_ok { + let resp = JsonRpcResponse::error( + req.id, + INVALID_REQUEST, + "missing, invalid, or expired session; call initialize first", + ); + return McpResult::Response { + body: serde_json::to_vec(&resp).unwrap_or_default(), + session_id: None, + }; } } @@ -361,8 +363,9 @@ mod tests { #[test] fn tools_list_returns_mcp_enabled_tools() { let server = make_server(); + let sid = server.session_store.create(None); let body = br#"{"jsonrpc":"2.0","id":2,"method":"tools/list"}"#; - match server.handle_request(body, None) { + match server.handle_request(body, Some(&sid)) { McpResult::Response { body: resp_body, .. } => { @@ -379,9 +382,10 @@ mod tests { #[test] fn tools_call_returns_needs_dispatch() { let server = make_server(); + let sid = server.session_store.create(None); let body = br#"{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"getHealth"}}"#; - match server.handle_request(body, None) { + match server.handle_request(body, Some(&sid)) { McpResult::NeedsDispatch { operation_index, path, @@ -397,9 +401,10 @@ mod tests { #[test] fn tools_call_unknown_tool() { let server = make_server(); + let sid = server.session_store.create(None); let body = br#"{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"nonexistent"}}"#; - match server.handle_request(body, None) { + match server.handle_request(body, Some(&sid)) { McpResult::Response { body: resp_body, .. } => { @@ -414,8 +419,9 @@ mod tests { #[test] fn unknown_method_returns_error() { let server = make_server(); + let sid = server.session_store.create(None); let body = br#"{"jsonrpc":"2.0","id":4,"method":"resources/list"}"#; - match server.handle_request(body, None) { + match server.handle_request(body, Some(&sid)) { McpResult::Response { body: resp_body, .. } => { @@ -440,8 +446,9 @@ mod tests { #[test] fn ping_returns_empty_result() { let server = make_server(); + let sid = server.session_store.create(None); let body = br#"{"jsonrpc":"2.0","id":5,"method":"ping"}"#; - match server.handle_request(body, None) { + match server.handle_request(body, Some(&sid)) { McpResult::Response { body: resp_body, .. } => { @@ -453,6 +460,27 @@ mod tests { } } + #[test] + fn non_initialize_without_session_is_rejected() { + // DP-1 regression: omitting the session must NOT bypass validation. + let server = make_server(); + for body in [ + &br#"{"jsonrpc":"2.0","id":2,"method":"tools/list"}"#[..], + &br#"{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"getHealth"}}"#[..], + ] { + match server.handle_request(body, None) { + McpResult::Response { + body: resp_body, .. + } => { + let json: serde_json::Value = + serde_json::from_slice(&resp_body).expect("valid json"); + assert_eq!(json["error"]["code"], INVALID_REQUEST); + } + _ => panic!("expected session-rejection Response"), + } + } + } + #[test] fn invalid_json_returns_parse_error() { let server = make_server(); @@ -532,8 +560,9 @@ mod tests { #[test] fn tools_call_missing_name_field() { let server = make_server(); + let sid = server.session_store.create(None); let body = br#"{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{}}"#; - match server.handle_request(body, None) { + match server.handle_request(body, Some(&sid)) { McpResult::Response { body: resp_body, .. } => { @@ -585,9 +614,10 @@ mod tests { server_version: None, }; let server = McpServer::new(&ops, &config); + let sid = server.session_store.create(None); let body = br#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"getUser","arguments":{"id":"123"}}}"#; - match server.handle_request(body, None) { + match server.handle_request(body, Some(&sid)) { McpResult::NeedsDispatch { operation_index, path, diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 369a11c..073720c 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -27,6 +27,7 @@ # Reference - [CLI reference](reference/cli.md) +- [Configuration & environment variables](reference/configuration.md) - [Spec Extensions](reference/extensions.md) - [Artifact Format](reference/artifact.md) - [Reserved Endpoints](reference/endpoints.md) @@ -36,3 +37,4 @@ - [Architecture](contributing/architecture.md) - [Development setup](contributing/development.md) - [WASM plugin development](contributing/plugins.md) +- [Security testing](contributing/security-testing.md) diff --git a/docs/contributing/security-testing.md b/docs/contributing/security-testing.md new file mode 100644 index 0000000..0491f7b --- /dev/null +++ b/docs/contributing/security-testing.md @@ -0,0 +1,169 @@ +# Security testing + +Barbacane ships a dedicated security testing framework with three layers: + +1. an **adversarial integration suite** that both regression-locks known + findings and helps discover new ones, +2. a set of **cargo-fuzz targets** for the highest-value parser / loader / + memory surfaces, and +3. this documentation plus a `make security-test` entry point. + +The suite is designed to be **red today, green as fixes land**: every test +asserts the *secure* (hardened) behaviour, so a failing test is a real, tracked +security gap. Each red test carries a comment of the form +`// EXPECTED TO FAIL until is fixed`. + +## Threat model categories + +| Finding | Category | What it locks down | +|--------------|-----------------------|--------------------| +| BARB-SEC-001 | Authz / IDOR | Every mutating control-plane route requires an admin Bearer token (`BARBACANE_CONTROL_ADMIN_TOKEN`); `/health` and `/ws/data-plane` are exempt. Project A cannot read/mutate project B's resources. | +| BARB-SEC-002 | SSRF | The WASM host HTTP client refuses to connect to loopback / link-local / private / metadata IPs, including after redirects (resolve-then-check). | +| BARB-SEC-003 | DoS / resource limits | Oversized chunked bodies → 413 without full buffering; slowloris read-timeout; 404 floods do not create one Prometheus series per raw path (sentinel ``). | +| BARB-SEC-004 | Sandbox / capability | A plugin granted only `log` cannot call other host functions — the linker is per-plugin / default-deny. | +| BARB-SEC-005 | Crypto / auth | JWT `alg:none`, expired `exp`, tampered signature, and wrong `aud` are rejected; a forged `X-Forwarded-For` does not bypass `ip-restriction` or reset rate-limit buckets. | +| BARB-SEC-006 | Artifact integrity | The `.bca` load path verifies per-entry SHA-256 against the manifest and an Ed25519 signature against `BARBACANE_TRUSTED_PUBKEY`; a single flipped byte fails the load. | + +## Layer 1 — Adversarial integration suite + +The suite lives under `crates/barbacane-test/tests/`: + +``` +crates/barbacane-test/tests/ + security.rs # test-binary root + shared helpers + security/ + authz.rs # BARB-SEC-001 + ssrf.rs # BARB-SEC-002 + dos.rs # BARB-SEC-003 + sandbox.rs # BARB-SEC-004 + crypto_auth.rs # BARB-SEC-005 + artifact_integrity.rs # BARB-SEC-006 +``` + +Fixtures specific to security tests live in `tests/fixtures/security/` (with +their own `barbacane.yaml` manifest). + +### Running it + +```bash +# Build the binaries the harness drives as subprocesses. +cargo build -p barbacane # data plane (most categories) +cargo build -p barbacane-control # control plane (BARB-SEC-001) + +# Build WASM plugins (the data-plane fixtures bundle them). +make plugins + +# Run just the security suite. +cargo test -p barbacane-test --test security +``` + +**Docker / service requirements** (the same as the rest of the integration +suite — see [Development setup](development.md)): + +- Most categories boot the **data plane** (`barbacane serve`) as a subprocess. + They need the `barbacane` binary built and the fixture WASM plugins present. +- **BARB-SEC-001** boots the **control plane** (`barbacane-control serve`), + which needs PostgreSQL. Start it with `make db-up` and export `DATABASE_URL`. + When PostgreSQL or the binary is unavailable the authz tests **skip** (they + print a `skip:` line and return) rather than fail spuriously. + +It is expected that security tests **fail at runtime today** — that is the +point. They turn green as each finding is fixed. A handful are `#[ignore]`d +because they need an src-side change first (see below); each carries a +`// BLOCKED: …` comment explaining exactly what. + +### What needs to be exposed for the parked tests + +A few tests are `#[ignore]`d with a precise blocker: + +- **Sandbox (BARB-SEC-004)** — needs (a) an adversarial fixture plugin that + declares only `log` but imports `host_http_call`, and (b) a per-plugin + default-deny linker in `barbacane-wasm` (today `add_host_functions` registers + *all* host functions). The load-time positive control additionally needs + `barbacane-wasm` added as a dev-dependency of `barbacane-test` so + `validate::validate_imports` can be called directly. +- **Crypto (BARB-SEC-005)** — the "validly-signed JWT is accepted" case needs + real RS256 signature verification (`public_key_pem`) in the `jwt-auth` plugin + plus a matching private key in the fixture to sign with. +- **DoS slowloris (BARB-SEC-003)** — needs a configurable + observable + read/header timeout to assert against without flaky wall-clock sleeps. +- **Authz IDOR phase 2 (BARB-SEC-001)** — needs per-project credentials and + ownership checks on the global `/specs/{id}` and `/artifacts/{id}` routes. + +## Layer 2 — Fuzz targets (cargo-fuzz) + +The fuzz crate is a **standalone** crate at the repo root (`fuzz/`). It uses the +empty-`[workspace]`-table trick so it is *not* part of the parent Cargo +workspace — no edit to the root `Cargo.toml` `exclude` list is needed, and +`cargo build` at the repo root never pulls it in. + +``` +fuzz/ + Cargo.toml + fuzz_targets/ + spec_parser.rs # barbacane_compiler::parse_spec — hostile OpenAPI/AsyncAPI + artifact_loader.rs # .bca gzip/tar loaders — decompression bombs / malformed archives + jsonrpc.rs # MCP JsonRpcRequest deserialization + validator.rs # OperationValidator::validate_request (+ percent-decoder) + wasm_host_memory.rs # guest-slice bounds — BLOCKED, see below +``` + +### Prerequisites and running + +```bash +rustup toolchain install nightly +cargo install cargo-fuzz + +# From the fuzz/ directory: +cd fuzz +cargo +nightly fuzz run spec_parser +cargo +nightly fuzz run artifact_loader +cargo +nightly fuzz run jsonrpc +cargo +nightly fuzz run validator +# cargo +nightly fuzz run wasm_host_memory # currently inert — see below +``` + +> Note: `cargo-fuzz` and a nightly toolchain are required to actually *fuzz*. +> The targets also build on stable (`cd fuzz && cargo build --bins`), which is +> what CI / pre-push uses to ensure they don't bit-rot. + +### Maintainer actions to sharpen the targets + +- **`validator`** reaches the percent-decoder *indirectly* through + `validate_request` because `urlencoding_decode` is private in + `crates/barbacane/src/validator.rs`. For a tighter, faster target, expose a + thin wrapper: + ```rust + pub fn fuzz_urldecode(s: &str) -> String { urlencoding_decode(s) } + ``` +- **`artifact_loader`** writes fuzz bytes to a temp file because the loaders take + `&Path`. A `pub fn load_*_from_reader(r: R)` (or a + `decompress_artifact(r) -> …`) in + `crates/barbacane-compiler/src/artifact.rs` would let the target fuzz + in-memory. +- **`wasm_host_memory`** is currently an inert stub. The guest-slice bounds + check is duplicated inline in every host-function closure in + `crates/barbacane-wasm/src/instance.rs` and is not reachable as a pure + function. Extract it, e.g.: + ```rust + pub fn guest_slice_bounds(ptr: i32, len: i32, mem_len: usize) + -> Result<(usize, usize), GuestMemoryError>; + ``` + then add `barbacane-wasm` to `fuzz/Cargo.toml` and call it from the target + (the intended body is sketched in the target's module docs). + +## Adding a new security test + +1. Pick (or open) a finding ID, e.g. `BARB-SEC-007`. +2. Add the test to the matching category file under + `crates/barbacane-test/tests/security/`, or create a new category module and + register it in the `mod security { … }` block in `tests/security.rs`. +3. Assert the **secure** behaviour. Add the marker comment: + `// EXPECTED TO FAIL until BARB-SEC-007 is fixed`. +4. If the test cannot compile/run against current APIs, mark it `#[ignore]` with + a `// BLOCKED: needs exposed` comment rather than leaving it broken. +5. Put any new fixtures under `tests/fixtures/security/` and declare the plugins + they use in `tests/fixtures/security/barbacane.yaml`. +6. Keep tests deterministic — avoid wall-clock sleeps; note any that are + unavoidable. +7. Update the threat-model table above. diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md new file mode 100644 index 0000000..cdac031 --- /dev/null +++ b/docs/reference/configuration.md @@ -0,0 +1,64 @@ +# Configuration & environment variables + +Barbacane is configured through CLI flags (see the [CLI reference](cli.md)) and a +small set of environment variables. The security-related variables below were +introduced by the security hardening pass and several change default behavior in +breaking but deliberate ways — Barbacane fails closed rather than running +insecurely. + +## Security environment variables + +| Variable | Component | Default | Effect | +|----------|-----------|---------|--------| +| `BARBACANE_CONTROL_ADMIN_TOKEN` | Control plane | _unset_ | **Required.** Bearer token that must accompany every control-plane API request (except `GET /health` and the data-plane WebSocket). The server refuses to start if unset. | +| `BARBACANE_CONTROL_ALLOWED_ORIGINS` | Control plane | _unset (no cross-origin)_ | Comma-separated CORS allowlist of browser origins permitted to call the API, e.g. `https://ui.example.com`. | +| `BARBACANE_TRUSTED_PUBKEY` | Data plane | _unset_ | Hex-encoded Ed25519 public key. When set, the data plane requires every loaded `.bca` artifact to carry a valid signature produced by the matching private key; load fails otherwise. When unset, the artifact's content hashes are still verified, but signature checking is skipped (a startup warning is logged). | +| `BARBACANE_SIGNING_KEY` | Compiler | _unset_ | Path to a PKCS#8 Ed25519 private key. When set, `barbacane compile` signs the artifact's content hash. When unset, the artifact is built unsigned. | +| `BARBACANE_SECRETS_DIR` | Data plane | _unset_ | Base directory that `file://` secret references are confined to. **Required to use `file://` secrets** — references are rejected when it is unset, and any path resolving outside this directory (after symlink/`..` resolution) is rejected. | +| `BARBACANE_ALLOW_INTERNAL_EGRESS` | Data plane | `false` | Set to `1`/`true` to disable the plugin SSRF guard and allow plugin HTTP calls to internal/loopback/link-local/cloud-metadata addresses. Leave off unless you have legitimate internal upstreams. | + +## Breaking-by-design defaults + +These changes are intentional secure defaults. Adopt them as follows: + +1. **The control plane will not start without `BARBACANE_CONTROL_ADMIN_TOKEN`.** + Generate a strong random token and pass it to every client as + `Authorization: Bearer `. Previously the API was unauthenticated. + +2. **`file://` secrets require `BARBACANE_SECRETS_DIR`.** If you reference + secrets like `file:///run/secrets/api-key`, set + `BARBACANE_SECRETS_DIR=/run/secrets`. `env://` references are unaffected. + +3. **MCP clients must initialize a session.** Non-`initialize` MCP requests + (`tools/list`, `tools/call`, …) without a valid `Mcp-Session-Id` are now + rejected; call `initialize` first and reuse the returned session id. + +4. **Plugin HTTP calls to internal addresses are blocked by default.** If a + plugin legitimately calls an internal upstream, set + `BARBACANE_ALLOW_INTERNAL_EGRESS=1` (or prefer an explicit allowlist when one + is available). + +5. **Plugins may only use their declared capabilities.** A plugin whose WASM + imports a host function outside the capabilities declared in its + `plugin.toml` fails to load. Official plugins already declare the correct + capabilities; custom plugins must list theirs under + `[capabilities] host_functions = [...]`. + +## Artifact signing quickstart + +```bash +# 1. Generate a dev keypair (PKCS#8 Ed25519). Any tool that emits PKCS#8 works; +# keep the private key secret and distribute only the public key. + +# 2. Sign at compile time: +BARBACANE_SIGNING_KEY=/path/to/ed25519.pk8 \ + barbacane compile -m barbacane.yaml -o api.bca + +# 3. Require verification on the data plane (pin the public key): +BARBACANE_TRUSTED_PUBKEY= \ + barbacane serve --artifact api.bca --listen 0.0.0.0:8080 +``` + +The signature covers the artifact's content hash, which binds every spec, route, +and plugin WASM checksum, so any tampering with the artifact fails verification +on load. diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock new file mode 100644 index 0000000..eea4c77 --- /dev/null +++ b/fuzz/Cargo.lock @@ -0,0 +1,4952 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59317f77929f0e679d39364702289274de2f0f0b22cbf50b2b8cff2169a0b27a" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "async-compression" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79b3f8a79cccc2898f31920fc69f304859b3bd567490f75ebf51ae1c792a9ac" +dependencies = [ + "compression-codecs", + "compression-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-nats" +version = "0.47.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07d6f157065c3461096d51aacde0c326fa49f3f6e0199e204c566842cdaa5299" +dependencies = [ + "base64", + "bytes", + "futures-util", + "memchr", + "nkeys", + "nuid", + "pin-project", + "portable-atomic", + "rand 0.8.6", + "regex", + "ring", + "rustls-native-certs", + "rustls-pki-types", + "rustls-webpki", + "serde", + "serde_json", + "serde_nanos", + "serde_repr", + "thiserror 1.0.69", + "time", + "tokio", + "tokio-rustls", + "tokio-stream", + "tokio-util", + "tokio-websockets", + "tracing", + "tryhard", + "url", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-lc-rs" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower 0.5.3", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", +] + +[[package]] +name = "barbacane" +version = "0.7.0" +dependencies = [ + "arc-swap", + "barbacane-compiler", + "barbacane-telemetry", + "barbacane-wasm", + "bytes", + "clap", + "futures-util", + "hex", + "http-body-util", + "hyper", + "hyper-util", + "jsonschema", + "notify", + "parking_lot", + "reqwest", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "serde_yaml", + "sha2", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tokio-rustls", + "tokio-tungstenite", + "tracing", + "uuid", +] + +[[package]] +name = "barbacane-compiler" +version = "0.7.0" +dependencies = [ + "chrono", + "flate2", + "hex", + "home", + "reqwest", + "serde", + "serde_json", + "serde_yaml", + "sha2", + "tar", + "thiserror 2.0.18", + "toml 0.8.23", + "tracing", +] + +[[package]] +name = "barbacane-fuzz" +version = "0.0.0" +dependencies = [ + "arbitrary", + "barbacane", + "barbacane-compiler", + "libfuzzer-sys", + "serde_json", + "tempfile", +] + +[[package]] +name = "barbacane-plugin-macros" +version = "0.7.0" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "barbacane-plugin-sdk" +version = "0.7.0" +dependencies = [ + "barbacane-plugin-macros", + "base64", + "serde", +] + +[[package]] +name = "barbacane-telemetry" +version = "0.7.0" +dependencies = [ + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry_sdk", + "prometheus-client", + "thiserror 2.0.18", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "barbacane-wasm" +version = "0.7.0" +dependencies = [ + "anyhow", + "async-nats", + "barbacane-plugin-sdk", + "barbacane-telemetry", + "base64", + "bytes", + "chrono", + "dashmap", + "futures-util", + "hex", + "jsonschema", + "parking_lot", + "regex-lite", + "reqwest", + "ring", + "rskafka", + "semver", + "serde", + "serde_json", + "sha2", + "thiserror 2.0.18", + "tokio", + "tokio-tungstenite", + "toml 0.8.23", + "tracing", + "uuid", + "wasmtime", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "borrow-or-share" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +dependencies = [ + "allocator-api2", +] + +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + +[[package]] +name = "bytes" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +dependencies = [ + "serde", +] + +[[package]] +name = "cc" +version = "1.2.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "compression-codecs" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2548391e9c1929c21bf6aa2680af86fe4c1b33e6cea9ac1cfeec0bd11218cf" +dependencies = [ + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpp_demangle" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cranelift-assembler-x64" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc822414b18d1f5b1b33ce1441534e311e62fef86ebb5b9d382af857d0272c9" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c646808b06f4532478d8d6057d74f15c3322f10d995d9486e7dcea405bf521a" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5996f01a686b2349cdb379083ec5ad3e8cb8767fb2d495d3a4f2ee4163a18d" +dependencies = [ + "cranelift-entity", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-bitset" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523fea83273f6a985520f57788809a4de2165794d9ab00fb1254fceb4f5aa00c" +dependencies = [ + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d73d1e372730b5f64ed1a2bd9f01fe4686c8ec14a28034e3084e530c8d951878" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.16.1", + "libm", + "log", + "pulley-interpreter", + "regalloc2", + "rustc-hash", + "serde", + "smallvec", + "target-lexicon", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0319c18165e93dc1ebf78946a8da0b1c341c95b4a39729a69574671639bdb5f" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", + "pulley-interpreter", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9195cd8aeecb55e401aa96b2eaa55921636e8246c127ed7908f7ef7e0d40f270" + +[[package]] +name = "cranelift-control" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8976c2154b74136322befc74222ab5c7249edd7e2604f8cbef2b94975541ffb9" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6038b3147c7982f4951150d5f96c7c06c1e7214b99d4b4a98607aadf8ded89d1" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", + "wasmtime-internal-core", +] + +[[package]] +name = "cranelift-frontend" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbd294abe236e23cc3d907b0936226b6a8342db7636daa9c7c72be1e323420e" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a90b6ed3aba84189352a87badeb93b2126d3724225a42dc67fdce53d1b139c" + +[[package]] +name = "cranelift-native" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3ec0cc1a54e22925eacf4fc3dc815f907734d3b377899d19d52bec04863e853" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.130.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "948865622f87f30907bb46fbb081b235ae63c1896a99a83c26a003305c1fa82d" + +[[package]] +name = "crc32c" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a47af21622d091a8f0fb295b88bc886ac74efcc613efc19f5d0b21de5c89e47" +dependencies = [ + "rustc_version", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dashmap" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "sha2", + "signature", + "subtle", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "email_address" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" +dependencies = [ + "serde", +] + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fancy-regex" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fluent-uri" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5" +dependencies = [ + "borrow-or-share", + "ref-cast", + "serde", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fraction" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e076045bb43dac435333ed5f04caf35c7463631d0dae2deb2638d94dd0a5b872" +dependencies = [ + "lazy_static", + "num", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "fxprof-processed-profile" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25234f20a3ec0a962a61770cfe39ecf03cb529a6e474ad8cff025ed497eda557" +dependencies = [ + "bitflags 2.13.0", + "debugid", + "rustc-hash", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "gimli" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7f043f89559805f8c7cacc432749b2fa0d0a0a9ee46ce47164ed5ba7f126c" +dependencies = [ + "fnv", + "hashbrown 0.16.1", + "indexmap 2.14.0", + "stable_deref_trait", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "h2" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.14.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash", + "serde", + "serde_core", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.8", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.4", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "im-rc" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1955a75fa080c677d3972822ec4bad316169ab1cfc6c257a942c2265dbe5fe" +dependencies = [ + "bitmaps", + "rand_core 0.6.4", + "rand_xoshiro", + "sized-chunks", + "typenum", + "version_check", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inotify" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "integer-encoding" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c00403deb17c3221a1fe4fb571b9ed0370b3dcd116553c77fa294a3d918699" + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "ittapi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b996fe614c41395cdaedf3cf408a9534851090959d90d54a535f675550b64b1" +dependencies = [ + "anyhow", + "ittapi-sys", + "log", +] + +[[package]] +name = "ittapi-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5385394064fa2c886205dba02598013ce83d3e92d33dbdc0c52fe0e7bf4fc" +dependencies = [ + "cc", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "jsonschema" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26a960f0c34d5423581d858ce94815cc11f0171b09939409097969ed269ede1b" +dependencies = [ + "ahash", + "base64", + "bytecount", + "email_address", + "fancy-regex", + "fraction", + "idna", + "itoa", + "num-cmp", + "once_cell", + "percent-encoding", + "referencing", + "regex-syntax", + "reqwest", + "serde", + "serde_json", + "uuid-simd", +] + +[[package]] +name = "kqueue" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "273c0752728918e0ac4976f2b275b6fefb9ecd400585dec929419f3844cd87b5" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" +dependencies = [ + "bitflags 2.13.0", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "lz4" +version = "1.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a20b523e860d03443e98350ceaac5e71c6ba89aea7d960769ec3ce37f4de5af4" +dependencies = [ + "lz4-sys", +] + +[[package]] +name = "lz4-sys" +version = "1.11.1+lz4-1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nkeys" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879011babc47a1c7fdf5a935ae3cfe94f34645ca0cac1c7f6424b36fc743d1bf" +dependencies = [ + "data-encoding", + "ed25519", + "ed25519-dalek", + "getrandom 0.2.17", + "log", + "rand 0.8.6", + "signatory", +] + +[[package]] +name = "notify" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009" +dependencies = [ + "bitflags 2.13.0", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.52.0", +] + +[[package]] +name = "notify-types" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585d3cb5e12e01aed9e8a1f70d5c6b5e86fe2a6e48fc8cd0b3e0b8df6f6eb174" +dependencies = [ + "instant", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "nuid" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc895af95856f929163a0aa20c26a78d26bfdc839f51b9d5aa7a5b79e52b7e83" +dependencies = [ + "rand 0.8.6", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.38.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271638cd5fa9cca89c4c304675ca658efc4e64a66c716b7cfe1afb4b9611dbbc" +dependencies = [ + "crc32fast", + "hashbrown 0.16.1", + "indexmap 2.14.0", + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "opentelemetry" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab70038c28ed37b97d8ed414b6429d343a8bbf44c9f79ec854f3a643029ba6d7" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror 1.0.69", + "tracing", +] + +[[package]] +name = "opentelemetry-http" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a8a7f5f6ba7c1b286c2fbca0454eaba116f63bbe69ed250b642d36fbb04d80" +dependencies = [ + "async-trait", + "bytes", + "http", + "opentelemetry", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91cf61a1868dacc576bf2b2a1c3e9ab150af7272909e80085c3173384fe11f76" +dependencies = [ + "async-trait", + "futures-core", + "http", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-proto", + "opentelemetry_sdk", + "prost", + "thiserror 1.0.69", + "tokio", + "tonic", + "tracing", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6e05acbfada5ec79023c85368af14abd0b307c015e9064d249b2a950ef459a6" +dependencies = [ + "opentelemetry", + "opentelemetry_sdk", + "prost", + "tonic", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231e9d6ceef9b0b2546ddf52335785ce41252bc7474ee8ba05bfad277be13ab8" +dependencies = [ + "async-trait", + "futures-channel", + "futures-executor", + "futures-util", + "glob", + "opentelemetry", + "percent-encoding", + "rand 0.8.6", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tokio-stream", + "tracing", +] + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap 2.14.0", +] + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prometheus-client" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504ee9ff529add891127c4827eb481bd69dc0ebc72e9a682e187db4caa60c3ca" +dependencies = [ + "dtoa", + "itoa", + "parking_lot", + "prometheus-client-derive-encode", +] + +[[package]] +name = "prometheus-client-derive-encode" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pulley-interpreter" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ec12fe19a9588315a49fe5704502a9c02d6a198303314b0c7c86123b06d29e5" +dependencies = [ + "cranelift-bitset", + "log", + "pulley-macros", + "wasmtime-internal-core", +] + +[[package]] +name = "pulley-macros" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f7d5ef31ebf1b46cd7e722ffef934e670d7e462f49aa01cde07b9b76dca580" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2 0.6.4", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.4", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "referencing" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8e15af8558cb157432dd3d88c1d1e982d0a5755cf80ce593b6499260aebc49" +dependencies = [ + "ahash", + "fluent-uri", + "once_cell", + "percent-encoding", + "serde_json", +] + +[[package]] +name = "regalloc2" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de2c52737737f8609e94f975dee22854a2d5c125772d4b1cf292120f4d45c186" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.17.1", + "log", + "rustc-hash", + "smallvec", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower 0.5.3", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots 1.0.8", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsasl" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed828a88913fd477c73bc3768b05d4b335ee775e29f2397bb59b9a4dd69ffb83" +dependencies = [ + "base64", + "digest", + "hmac", + "pbkdf2", + "rand 0.8.6", + "serde", + "serde_json", + "sha2", + "stringprep", + "thiserror 2.0.18", +] + +[[package]] +name = "rskafka" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "849b87417a191e37e16b8893eba8928bbd10e1989e6b57c4f8531549eaa29bdc" +dependencies = [ + "bytes", + "chrono", + "crc32c", + "flate2", + "futures", + "integer-encoding", + "lz4", + "parking_lot", + "rand 0.8.6", + "rsasl", + "snap", + "thiserror 1.0.69", + "tokio", + "tracing", + "zstd", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_nanos" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a93142f0367a4cc53ae0fead1bcda39e85beccfad3dcd717656cacab94b12985" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.14.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signatory" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e303f8205714074f6068773f0e29527e0453937fe837c9717d066635b65f31" +dependencies = [ + "pkcs8", + "rand_core 0.6.4", + "signature", + "zeroize", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] + +[[package]] +name = "snap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b6b67fb9a61334225b5b790716f609cd58395f895b3fe8b328786812a40bc3b" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.4", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite", + "webpki-roots 0.26.11", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-websockets" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f591660438b3038dd04d16c938271c79e7e06260ad2ea2885a4861bfb238605d" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-sink", + "http", + "httparse", + "rand 0.8.6", + "ring", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tokio-util", + "webpki-roots 0.26.11", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tonic" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "prost", + "socket2 0.5.10", + "tokio", + "tokio-stream", + "tower 0.4.13", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand 0.8.6", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "async-compression", + "bitflags 2.13.0", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "pin-project-lite", + "tokio", + "tokio-util", + "tower 0.5.3", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tryhard" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fe58ebd5edd976e0fe0f8a14d2a04b7c81ef153ea9a54eebc42e67c2c23b4e5" +dependencies = [ + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.4", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror 2.0.18", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "uuid-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" +dependencies = [ + "outref", + "uuid", + "vsimd", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-compose" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd23d12cc95c451c1306db5bc63075fbebb612bb70c53b4237b1ce5bc178343" +dependencies = [ + "anyhow", + "heck", + "im-rc", + "indexmap 2.14.0", + "log", + "petgraph", + "serde", + "serde_derive", + "serde_yaml", + "smallvec", + "wasm-encoder 0.245.1", + "wasmparser 0.245.1", + "wat", +] + +[[package]] +name = "wasm-encoder" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9dca005e69bf015e45577e415b9af8c67e8ee3c0e38b5b0add5aa92581ed5c" +dependencies = [ + "leb128fmt", + "wasmparser 0.245.1", +] + +[[package]] +name = "wasm-encoder" +version = "0.252.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8185ae345fa5687c054626ff9a50e7089797a343d9904d1dc9820eb4c4d3196f" +dependencies = [ + "leb128fmt", + "wasmparser 0.252.0", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f08c9adee0428b7bddf3890fc27e015ac4b761cc608c822667102b8bfd6995e" +dependencies = [ + "bitflags 2.13.0", + "hashbrown 0.16.1", + "indexmap 2.14.0", + "semver", + "serde", +] + +[[package]] +name = "wasmparser" +version = "0.252.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3eb099dcadcde5be9eef55e3a337128efd4e44b4c93122487e4d2e4e1c6627c" +dependencies = [ + "bitflags 2.13.0", + "indexmap 2.14.0", + "semver", +] + +[[package]] +name = "wasmprinter" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41517a3716fbb8ccf46daa9c1325f760fcbff5168e75c7392288e410b91ac8" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.245.1", +] + +[[package]] +name = "wasmtime" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efb1ed5899dde98357cfdcf647a4614498798719793898245b4b34e663addabf" +dependencies = [ + "addr2line", + "async-trait", + "bitflags 2.13.0", + "bumpalo", + "cc", + "cfg-if", + "encoding_rs", + "futures", + "fxprof-processed-profile", + "gimli", + "ittapi", + "libc", + "log", + "mach2", + "memfd", + "object", + "once_cell", + "postcard", + "pulley-interpreter", + "rayon", + "rustix", + "semver", + "serde", + "serde_derive", + "serde_json", + "smallvec", + "target-lexicon", + "tempfile", + "wasm-compose", + "wasm-encoder 0.245.1", + "wasmparser 0.245.1", + "wasmtime-environ", + "wasmtime-internal-cache", + "wasmtime-internal-component-macro", + "wasmtime-internal-component-util", + "wasmtime-internal-core", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", + "wasmtime-internal-winch", + "wat", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-environ" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4172382dcc785c31d0e862c6780a18f5dd437914d22c4691351f965ef751c821" +dependencies = [ + "anyhow", + "cpp_demangle", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-entity", + "gimli", + "hashbrown 0.16.1", + "indexmap 2.14.0", + "log", + "object", + "postcard", + "rustc-demangle", + "semver", + "serde", + "serde_derive", + "sha2", + "smallvec", + "target-lexicon", + "wasm-encoder 0.245.1", + "wasmparser 0.245.1", + "wasmprinter", + "wasmtime-internal-component-util", + "wasmtime-internal-core", +] + +[[package]] +name = "wasmtime-internal-cache" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ed398988226d7aa0505ac6bb576e09532ad722d702ec4e66365d78ed695c95f" +dependencies = [ + "base64", + "directories-next", + "log", + "postcard", + "rustix", + "serde", + "serde_derive", + "sha2", + "toml 0.9.12+spec-1.1.0", + "wasmtime-environ", + "windows-sys 0.61.2", + "zstd", +] + +[[package]] +name = "wasmtime-internal-component-macro" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae5ec9fff073ff13b81732d56a9515d761c245750bcda09093827f84130ebc25" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", + "wit-parser", +] + +[[package]] +name = "wasmtime-internal-component-util" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "935d9ab293ba27d1ec9aa7bc1b3a43993dbe961af2a8f23f90a11e1331b4c13f" + +[[package]] +name = "wasmtime-internal-core" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3820b174f477d2a7083209d1ad5353fcdb11eaea434b2137b8681029460dd3" +dependencies = [ + "anyhow", + "hashbrown 0.16.1", + "libm", + "serde", +] + +[[package]] +name = "wasmtime-internal-cranelift" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1679d205caf9766c6aa309d45bb3e7c634d7725e3164404df33824b9f7c4fb7" +dependencies = [ + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli", + "itertools", + "log", + "object", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror 2.0.18", + "wasmparser 0.245.1", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-fiber" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1e505254058be5b0df458d670ee42d9eafe2349d04c1296e9dc01071dc20a85" +dependencies = [ + "cc", + "cfg-if", + "libc", + "rustix", + "wasmtime-environ", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-jit-debug" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c2e05b345f1773e59c20e6ad7298fd6857cdea245023d88bb659c96d8f0ea72" +dependencies = [ + "cc", + "object", + "rustix", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-jit-icache-coherence" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b86701b234a4643e3f111869aa792b3a05a06e02d486ee9cb6c04dae16b52dab" +dependencies = [ + "cfg-if", + "libc", + "wasmtime-internal-core", + "windows-sys 0.61.2", +] + +[[package]] +name = "wasmtime-internal-unwinder" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63558d801beb83dde9b336eb4ae049019aee26627926edb32cd119d7e4c83cd" +dependencies = [ + "cfg-if", + "cranelift-codegen", + "log", + "object", + "wasmtime-environ", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "737c4d956fc3a848541a064afb683dd2771132a6b125be5baaf95c4379aa47df" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasmtime-internal-winch" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f599b79545e3bba0b7913406055ebede5bb0dabee9ba2015ef25a9f4c9f47807" +dependencies = [ + "cranelift-codegen", + "gimli", + "log", + "object", + "target-lexicon", + "wasmparser 0.245.1", + "wasmtime-environ", + "wasmtime-internal-cranelift", + "winch-codegen", +] + +[[package]] +name = "wasmtime-internal-wit-bindgen" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2192a77a00b9a67800c2b4e1c70fb6abca79d6b529e53a2ef9dcdcc36090330d" +dependencies = [ + "anyhow", + "bitflags 2.13.0", + "heck", + "indexmap 2.14.0", + "wit-parser", +] + +[[package]] +name = "wast" +version = "252.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "942a3449d6a593fccc111a6241c8df52bda168af30e40bf9580d4394d7374c65" +dependencies = [ + "bumpalo", + "leb128fmt", + "memchr", + "unicode-width", + "wasm-encoder 0.252.0", +] + +[[package]] +name = "wat" +version = "1.252.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c72a4ba7088f7bac94cf516e49882bdf97068904a563768cf249efc839ec42cb" +dependencies = [ + "wast", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.8", +] + +[[package]] +name = "webpki-roots" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winch-codegen" +version = "43.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dbb0cf07b0dfe7b7a1ca8efb8f94ba98bd0fb144c411ea1665c78f0449e958" +dependencies = [ + "cranelift-assembler-x64", + "cranelift-codegen", + "gimli", + "regalloc2", + "smallvec", + "target-lexicon", + "thiserror 2.0.18", + "wasmparser 0.245.1", + "wasmtime-environ", + "wasmtime-internal-core", + "wasmtime-internal-cranelift", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-parser" +version = "0.245.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330698718e82983499419494dd1e3d7811a457a9bf9f69734e8c5f07a2547929" +dependencies = [ + "anyhow", + "hashbrown 0.16.1", + "id-arena", + "indexmap 2.14.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.245.1", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 0000000..4f60b82 --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,84 @@ +# Standalone fuzz crate for Barbacane (Layer 2 of the security testing framework). +# +# This crate is deliberately NOT part of the parent Cargo workspace: the empty +# `[workspace]` table below makes Cargo treat `fuzz/` as its own workspace root, +# so `cargo build`/`cargo test` at the repo root never pulls it in (cargo-fuzz +# and a nightly toolchain are only needed here). This is the recommended +# cargo-fuzz layout and keeps the framework self-contained — no edit to the root +# Cargo.toml `exclude` list is required. +# +# Build / run (requires nightly + cargo-fuzz): +# rustup toolchain install nightly +# cargo install cargo-fuzz +# cargo +nightly fuzz run spec_parser +# +# See docs/contributing/security-testing.md for the full runbook. + +[package] +name = "barbacane-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +# Empty workspace table → standalone crate, excluded from the parent workspace. +[workspace] + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" +arbitrary = { version = "1", features = ["derive"] } +serde_json = "1" + +# Crates under test. Paths are relative to this fuzz/ directory. +barbacane-compiler = { path = "../crates/barbacane-compiler" } +# The data-plane crate exposes a library target named `barbacane_lib` +# (see crates/barbacane/Cargo.toml `[lib] name = "barbacane_lib"`), which is how +# we reach the validator and MCP JSON-RPC parser. +barbacane = { path = "../crates/barbacane" } + +# Used by the artifact_loader target to materialise fuzz input as a temp .bca +# (the loader API takes a &Path, not bytes). +tempfile = "3" + +[profile.release] +debug = 1 + +# Each fuzz target is its own binary. `test = false` / `doc = false` keep +# `cargo test` from trying to build them as ordinary tests. + +[[bin]] +name = "spec_parser" +path = "fuzz_targets/spec_parser.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "artifact_loader" +path = "fuzz_targets/artifact_loader.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "jsonrpc" +path = "fuzz_targets/jsonrpc.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "validator" +path = "fuzz_targets/validator.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "wasm_host_memory" +path = "fuzz_targets/wasm_host_memory.rs" +test = false +doc = false +bench = false diff --git a/fuzz/fuzz_targets/artifact_loader.rs b/fuzz/fuzz_targets/artifact_loader.rs new file mode 100644 index 0000000..9cb6704 --- /dev/null +++ b/fuzz/fuzz_targets/artifact_loader.rs @@ -0,0 +1,51 @@ +//! Fuzz the `.bca` artifact load path (gzip + tar decompression). +//! +//! Targets the loader functions in +//! `crates/barbacane-compiler/src/artifact.rs`: +//! * `load_manifest(&Path)` +//! * `load_routes(&Path)` +//! * `load_specs(&Path)` +//! * `load_plugins(&Path)` +//! +//! Each opens the file, wraps it in `flate2::read::GzDecoder` + `tar::Archive`, +//! and walks entries. Hostile input we care about: decompression bombs +//! (tiny gzip → huge expansion), malformed/oversized tar headers, path-traversal +//! entry names, truncated streams. The invariant is: no panic, no unbounded +//! memory blowup that OOM-kills the process for a small input. +//! +//! The loader API takes a `&Path` (there is no bytes/reader variant — see the +//! "maintainer action" note in docs/contributing/security-testing.md), so we +//! materialise the fuzz bytes to a temp file first. The libFuzzer max input size +//! bounds the *compressed* size; a decompression bomb that explodes from a small +//! seed is exactly what we want to surface. +//! +//! Run: `cargo +nightly fuzz run artifact_loader` + +#![no_main] + +use std::io::Write; + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + // Write the raw fuzz bytes as a candidate .bca and run every loader against + // it. We do NOT pre-validate gzip framing: malformed framing is part of the + // attack surface and must be handled gracefully by the loaders. + let Ok(mut tmp) = tempfile::NamedTempFile::new() else { + return; + }; + if tmp.write_all(data).is_err() { + return; + } + if tmp.flush().is_err() { + return; + } + let path = tmp.path(); + + // All four loaders share the gzip+tar walk; fuzz each so manifest/routes/ + // specs/plugins-specific JSON handling is also exercised. + let _ = barbacane_compiler::load_manifest(path); + let _ = barbacane_compiler::load_routes(path); + let _ = barbacane_compiler::load_specs(path); + let _ = barbacane_compiler::load_plugins(path); +}); diff --git a/fuzz/fuzz_targets/jsonrpc.rs b/fuzz/fuzz_targets/jsonrpc.rs new file mode 100644 index 0000000..5d7d6de --- /dev/null +++ b/fuzz/fuzz_targets/jsonrpc.rs @@ -0,0 +1,24 @@ +//! Fuzz the MCP JSON-RPC request parser. +//! +//! Target: deserialization of `barbacane_lib::mcp::jsonrpc::JsonRpcRequest` +//! (`crates/barbacane/src/mcp/jsonrpc.rs`), which is the wire-format type the +//! MCP endpoint parses every incoming request into via serde_json. +//! +//! The MCP endpoint accepts untrusted client JSON, so the parse step must never +//! panic or hang regardless of input: deeply nested params, gigantic numbers, +//! duplicate keys, non-UTF-8-escaped strings, etc. +//! +//! Invariant: parsing arbitrary bytes returns `Ok`/`Err`, never panics. +//! +//! Run: `cargo +nightly fuzz run jsonrpc` + +#![no_main] + +use libfuzzer_sys::fuzz_target; + +use barbacane_lib::mcp::jsonrpc::JsonRpcRequest; + +fuzz_target!(|data: &[u8]| { + // Parse exactly as the MCP endpoint does: serde_json over the raw bytes. + let _ = serde_json::from_slice::(data); +}); diff --git a/fuzz/fuzz_targets/spec_parser.rs b/fuzz/fuzz_targets/spec_parser.rs new file mode 100644 index 0000000..6612707 --- /dev/null +++ b/fuzz/fuzz_targets/spec_parser.rs @@ -0,0 +1,26 @@ +//! Fuzz the OpenAPI/AsyncAPI spec parser. +//! +//! Target: `barbacane_compiler::parse_spec(&str) -> Result` +//! (re-exported from `crates/barbacane-compiler/src/spec_parser/parser.rs`). +//! +//! Invariant: the parser must never panic, overflow the stack, or run away on +//! hostile input — it must always return `Ok`/`Err` for any UTF-8 string. +//! `parse_spec` runs serde_yaml + recursive path/channel walking, so deeply +//! nested YAML, huge anchor expansions, and malformed extension blocks are the +//! interesting cases. +//! +//! Run: `cargo +nightly fuzz run spec_parser` + +#![no_main] + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + // The parser takes &str; only feed it valid UTF-8 (invalid UTF-8 is a + // separate, uninteresting rejection at the boundary). + if let Ok(input) = std::str::from_utf8(data) { + // We only care that this does not panic / hang / overflow. The Result is + // intentionally ignored. + let _ = barbacane_compiler::parse_spec(input); + } +}); diff --git a/fuzz/fuzz_targets/validator.rs b/fuzz/fuzz_targets/validator.rs new file mode 100644 index 0000000..c6ef166 --- /dev/null +++ b/fuzz/fuzz_targets/validator.rs @@ -0,0 +1,82 @@ +//! Fuzz request validation and the percent-decoder. +//! +//! Target: `barbacane_lib::validator::OperationValidator::validate_request` +//! (`crates/barbacane/src/validator.rs`). `validate_request` walks path/query/ +//! header/body validation; the query path runs the private `urlencoding_decode` +//! percent-decoder, so feeding arbitrary query strings fuzzes the decoder +//! indirectly. +//! +//! Invariant: validation of arbitrary inputs never panics (notably the +//! percent-decoder must handle truncated `%`, non-hex digits, and overlong +//! sequences without panicking or slicing on a non-char-boundary). +//! +//! NOTE for the maintainer: the percent-decoder `urlencoding_decode` is +//! `fn` (private) in `validator.rs`. To fuzz it *directly* (tighter, faster), +//! expose a thin wrapper, e.g.: +//! `pub fn fuzz_urldecode(s: &str) -> String { urlencoding_decode(s) }` +//! Until then this target reaches it through `validate_request`, which is fine +//! but also exercises the surrounding validation machinery. +//! +//! Run: `cargo +nightly fuzz run validator` + +#![no_main] + +use std::collections::HashMap; + +use arbitrary::Arbitrary; +use libfuzzer_sys::fuzz_target; + +use barbacane_compiler::{Parameter, RequestBody}; +use barbacane_lib::validator::OperationValidator; + +/// Structured fuzz input so libFuzzer can explore each field independently. +#[derive(Debug, Arbitrary)] +struct Input { + query_string: String, + body: Vec, + content_type: Option, + // A few (name, value) header pairs. + headers: Vec<(String, String)>, + // Whether to attach a query parameter named after `param_name` so the + // query-decode path has a declared param to match. + declare_query_param: bool, + param_name: String, + require_body: bool, +} + +fuzz_target!(|input: Input| { + // Build a validator with optional declared parameters / body so different + // validation branches (and the percent-decoder) are reachable. + let mut params: Vec = Vec::new(); + if input.declare_query_param && !input.param_name.is_empty() { + params.push(Parameter { + name: input.param_name.clone(), + location: "query".to_string(), + required: false, + schema: None, + }); + } + + let request_body = if input.require_body { + Some(RequestBody { + required: true, + content: Default::default(), + }) + } else { + None + }; + + let validator = OperationValidator::new(¶ms, request_body.as_ref()); + + let headers: HashMap = input.headers.into_iter().collect(); + + // No path params; the interesting attacker-controlled surfaces are the query + // string (percent-decoder), headers, and body. + let _ = validator.validate_request( + &[], + Some(input.query_string.as_str()), + &headers, + input.content_type.as_deref(), + &input.body, + ); +}); diff --git a/fuzz/fuzz_targets/wasm_host_memory.rs b/fuzz/fuzz_targets/wasm_host_memory.rs new file mode 100644 index 0000000..b9af646 --- /dev/null +++ b/fuzz/fuzz_targets/wasm_host_memory.rs @@ -0,0 +1,61 @@ +//! Fuzz the WASM guest-slice read helper (ptr/len bounds checking). +//! +//! ## Status: BLOCKED — documented, intentionally inert. +//! +//! The guest-memory read pattern we want to fuzz lives INLINE inside every host +//! function closure registered by `add_host_functions` +//! (`crates/barbacane-wasm/src/instance.rs`), e.g.: +//! +//! ```ignore +//! let memory = caller.get_export("memory").and_then(|e| e.into_memory())?; +//! let data = memory.data(&caller); +//! if end > data.len() { return -1; } // <-- the bounds check +//! let slice = &data[start..end]; +//! ``` +//! +//! There is NO standalone, public `fn read_guest_slice(ptr, len, mem_len) -> ..` +//! to call: the check is duplicated per host function and is only reachable with +//! a live `wasmtime::Caller` + instantiated module + linear memory. Reaching it +//! from a libFuzzer target would mean instantiating a full plugin instance per +//! input, which is far too slow for fuzzing and pulls the entire runtime into +//! the harness. +//! +//! ### Maintainer action to ENABLE this target +//! +//! Extract the bounds arithmetic into a pure, public, side-effect-free helper in +//! `barbacane-wasm`, e.g.: +//! +//! ```ignore +//! /// Returns the valid `[start, end)` range, or an error if out of bounds. +//! pub fn guest_slice_bounds(ptr: i32, len: i32, mem_len: usize) +//! -> Result<(usize, usize), GuestMemoryError>; +//! ``` +//! +//! and have every host function call it. Then this target becomes: +//! +//! ```ignore +//! use arbitrary::Arbitrary; +//! #[derive(Arbitrary, Debug)] +//! struct In { ptr: i32, len: i32, mem_len: u32 } +//! fuzz_target!(|i: In| { +//! let _ = barbacane_wasm::guest_slice_bounds(i.ptr, i.len, i.mem_len as usize); +//! }); +//! ``` +//! +//! and `barbacane-wasm = { path = "../crates/barbacane-wasm" }` must be added to +//! `fuzz/Cargo.toml`. Until that helper is exposed, this target compiles and +//! runs but exercises nothing, so the framework stays buildable without an +//! src-side change. +//! +//! Run: `cargo +nightly fuzz run wasm_host_memory` (currently a no-op). + +#![no_main] + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|_data: &[u8]| { + // Intentionally inert. See the module docs above: the guest-slice bounds + // check is not reachable as a pure function without an src change in + // barbacane-wasm. This stub keeps the fuzz crate building with all five + // targets declared while clearly flagging the blocker for the maintainer. +}); diff --git a/plugins/acl/Cargo.lock b/plugins/acl/Cargo.lock index ac9c38e..75362a9 100644 --- a/plugins/acl/Cargo.lock +++ b/plugins/acl/Cargo.lock @@ -13,7 +13,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-macros" -version = "0.6.0" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -21,7 +21,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.0" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/plugins/acl/plugin.toml b/plugins/acl/plugin.toml index 3cdb13f..4ded388 100644 --- a/plugins/acl/plugin.toml +++ b/plugins/acl/plugin.toml @@ -4,3 +4,6 @@ version = "0.1.0" type = "middleware" description = "Access control list middleware — allow/deny by consumer and group" wasm = "acl.wasm" + +[capabilities] +host_functions = [] diff --git a/plugins/ai-cost-tracker/Cargo.lock b/plugins/ai-cost-tracker/Cargo.lock index 3e19fc5..9b90e41 100644 --- a/plugins/ai-cost-tracker/Cargo.lock +++ b/plugins/ai-cost-tracker/Cargo.lock @@ -13,7 +13,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-macros" -version = "0.6.3" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -21,7 +21,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.3" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/plugins/ai-cost-tracker/plugin.toml b/plugins/ai-cost-tracker/plugin.toml index e6724d6..eb5cee4 100644 --- a/plugins/ai-cost-tracker/plugin.toml +++ b/plugins/ai-cost-tracker/plugin.toml @@ -6,6 +6,4 @@ description = "Records per-request LLM cost (USD) based on token usage and a con wasm = "ai-cost-tracker.wasm" [capabilities] -log = true -context_get = true -telemetry = true +host_functions = ["log", "context_get", "telemetry"] diff --git a/plugins/ai-prompt-guard/Cargo.lock b/plugins/ai-prompt-guard/Cargo.lock index c2bf380..2dfcc5e 100644 --- a/plugins/ai-prompt-guard/Cargo.lock +++ b/plugins/ai-prompt-guard/Cargo.lock @@ -23,7 +23,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-macros" -version = "0.6.3" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -31,7 +31,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.3" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/plugins/ai-prompt-guard/plugin.toml b/plugins/ai-prompt-guard/plugin.toml index 4620a84..620789b 100644 --- a/plugins/ai-prompt-guard/plugin.toml +++ b/plugins/ai-prompt-guard/plugin.toml @@ -6,6 +6,5 @@ description = "Validates and constrains LLM prompts before dispatch. Named profi wasm = "ai-prompt-guard.wasm" [capabilities] -log = true -context_get = true +host_functions = ["log", "context_get"] body_access = true diff --git a/plugins/ai-proxy/Cargo.lock b/plugins/ai-proxy/Cargo.lock index b80bd19..64edc92 100644 --- a/plugins/ai-proxy/Cargo.lock +++ b/plugins/ai-proxy/Cargo.lock @@ -24,7 +24,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-macros" -version = "0.6.3" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -32,7 +32,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.3" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/plugins/ai-proxy/plugin.toml b/plugins/ai-proxy/plugin.toml index 6befacf..aa7a7fe 100644 --- a/plugins/ai-proxy/plugin.toml +++ b/plugins/ai-proxy/plugin.toml @@ -6,9 +6,4 @@ description = "AI gateway dispatcher — routes to LLM providers (OpenAI, Anthro wasm = "ai-proxy.wasm" [capabilities] -log = true -context_get = true -context_set = true -http_call = true -telemetry = true -clock_now = true +host_functions = ["log", "context_get", "context_set", "http_call", "telemetry", "clock_now"] diff --git a/plugins/ai-response-guard/Cargo.lock b/plugins/ai-response-guard/Cargo.lock index 72797e2..4408469 100644 --- a/plugins/ai-response-guard/Cargo.lock +++ b/plugins/ai-response-guard/Cargo.lock @@ -23,7 +23,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-macros" -version = "0.6.3" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -31,7 +31,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.3" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/plugins/ai-response-guard/plugin.toml b/plugins/ai-response-guard/plugin.toml index 0a344a0..bfd76ec 100644 --- a/plugins/ai-response-guard/plugin.toml +++ b/plugins/ai-response-guard/plugin.toml @@ -6,7 +6,5 @@ description = "Inspects LLM responses under a named policy profile (redact + blo wasm = "ai-response-guard.wasm" [capabilities] -log = true -context_get = true +host_functions = ["log", "context_get", "telemetry"] body_access = true -telemetry = true diff --git a/plugins/ai-token-limit/Cargo.lock b/plugins/ai-token-limit/Cargo.lock index b6797da..74f4240 100644 --- a/plugins/ai-token-limit/Cargo.lock +++ b/plugins/ai-token-limit/Cargo.lock @@ -13,7 +13,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-macros" -version = "0.6.3" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -21,7 +21,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.3" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/plugins/ai-token-limit/plugin.toml b/plugins/ai-token-limit/plugin.toml index 42ac2b2..ba04fc4 100644 --- a/plugins/ai-token-limit/plugin.toml +++ b/plugins/ai-token-limit/plugin.toml @@ -6,7 +6,4 @@ description = "Token-based rate limiting for LLM endpoints (ADR-0024). Budget is wasm = "ai-token-limit.wasm" [capabilities] -log = true -context_get = true -context_set = true -rate_limit = true +host_functions = ["log", "context_get", "context_set", "rate_limit"] diff --git a/plugins/apikey-auth/Cargo.lock b/plugins/apikey-auth/Cargo.lock index dee85c5..6670231 100644 --- a/plugins/apikey-auth/Cargo.lock +++ b/plugins/apikey-auth/Cargo.lock @@ -13,7 +13,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-macros" -version = "0.6.0" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -21,7 +21,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.0" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/plugins/apikey-auth/plugin.toml b/plugins/apikey-auth/plugin.toml index 7f948ea..b274146 100644 --- a/plugins/apikey-auth/plugin.toml +++ b/plugins/apikey-auth/plugin.toml @@ -4,3 +4,6 @@ version = "0.1.0" type = "middleware" description = "API key authentication middleware for validating API keys from headers or query parameters" wasm = "apikey-auth.wasm" + +[capabilities] +host_functions = [] diff --git a/plugins/basic-auth/Cargo.lock b/plugins/basic-auth/Cargo.lock index 4ab56da..1e1bf79 100644 --- a/plugins/basic-auth/Cargo.lock +++ b/plugins/basic-auth/Cargo.lock @@ -14,7 +14,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-macros" -version = "0.6.0" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -22,7 +22,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.0" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/plugins/basic-auth/plugin.toml b/plugins/basic-auth/plugin.toml index 6564117..b2ac0bf 100644 --- a/plugins/basic-auth/plugin.toml +++ b/plugins/basic-auth/plugin.toml @@ -4,3 +4,6 @@ version = "0.1.0" type = "middleware" description = "HTTP Basic authentication middleware (RFC 7617)" wasm = "basic-auth.wasm" + +[capabilities] +host_functions = [] diff --git a/plugins/bot-detection/Cargo.lock b/plugins/bot-detection/Cargo.lock index 75e37f0..63ddff8 100644 --- a/plugins/bot-detection/Cargo.lock +++ b/plugins/bot-detection/Cargo.lock @@ -13,7 +13,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-macros" -version = "0.6.0" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -21,7 +21,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.0" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/plugins/bot-detection/plugin.toml b/plugins/bot-detection/plugin.toml index e5e98f6..fdc7a98 100644 --- a/plugins/bot-detection/plugin.toml +++ b/plugins/bot-detection/plugin.toml @@ -6,4 +6,4 @@ description = "Block requests from known bots and scrapers by User-Agent pattern wasm = "bot-detection.wasm" [capabilities] -log = true +host_functions = [] diff --git a/plugins/cache/Cargo.lock b/plugins/cache/Cargo.lock index 3c4af35..708f483 100644 --- a/plugins/cache/Cargo.lock +++ b/plugins/cache/Cargo.lock @@ -13,7 +13,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-macros" -version = "0.6.0" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -21,7 +21,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.0" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/plugins/cache/plugin.toml b/plugins/cache/plugin.toml index 5ef0db2..d1081f5 100644 --- a/plugins/cache/plugin.toml +++ b/plugins/cache/plugin.toml @@ -5,5 +5,4 @@ type = "middleware" description = "Response caching middleware with TTL support" [capabilities] -cache = true -log = true +host_functions = ["log", "cache"] diff --git a/plugins/cel/Cargo.lock b/plugins/cel/Cargo.lock index 8888b9d..cf1ea0f 100644 --- a/plugins/cel/Cargo.lock +++ b/plugins/cel/Cargo.lock @@ -31,7 +31,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-macros" -version = "0.6.3" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -39,7 +39,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.3" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/plugins/cel/plugin.toml b/plugins/cel/plugin.toml index d34c10c..c2e81a7 100644 --- a/plugins/cel/plugin.toml +++ b/plugins/cel/plugin.toml @@ -6,6 +6,5 @@ description = "CEL policy evaluation middleware — inline expression-based acce wasm = "cel.wasm" [capabilities] -log = true -context_set = true +host_functions = ["log", "context_set"] body_access = true diff --git a/plugins/correlation-id/Cargo.lock b/plugins/correlation-id/Cargo.lock index 5429ed0..de60ce3 100644 --- a/plugins/correlation-id/Cargo.lock +++ b/plugins/correlation-id/Cargo.lock @@ -13,7 +13,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-macros" -version = "0.6.0" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -21,7 +21,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.0" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/plugins/correlation-id/plugin.toml b/plugins/correlation-id/plugin.toml index f9612d6..9128a9d 100644 --- a/plugins/correlation-id/plugin.toml +++ b/plugins/correlation-id/plugin.toml @@ -6,7 +6,4 @@ description = "Propagates or generates correlation IDs for distributed tracing" wasm = "correlation-id.wasm" [capabilities] -log = true -generate_uuid = true -context_get = true -context_set = true +host_functions = ["log", "context_get", "context_set", "generate_uuid"] diff --git a/plugins/cors/Cargo.lock b/plugins/cors/Cargo.lock index 3437469..b5bc48e 100644 --- a/plugins/cors/Cargo.lock +++ b/plugins/cors/Cargo.lock @@ -13,7 +13,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-macros" -version = "0.6.0" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -21,7 +21,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.0" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/plugins/cors/plugin.toml b/plugins/cors/plugin.toml index ac2d42f..9330a77 100644 --- a/plugins/cors/plugin.toml +++ b/plugins/cors/plugin.toml @@ -6,4 +6,4 @@ description = "CORS middleware for cross-origin resource sharing" wasm = "cors.wasm" [capabilities] -log = true +host_functions = ["log"] diff --git a/plugins/fire-and-forget/Cargo.lock b/plugins/fire-and-forget/Cargo.lock index f7a78fe..7a79e0b 100644 --- a/plugins/fire-and-forget/Cargo.lock +++ b/plugins/fire-and-forget/Cargo.lock @@ -13,7 +13,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-macros" -version = "0.6.1" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -21,7 +21,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.1" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/plugins/fire-and-forget/plugin.toml b/plugins/fire-and-forget/plugin.toml index e6933de..2a7a4f3 100644 --- a/plugins/fire-and-forget/plugin.toml +++ b/plugins/fire-and-forget/plugin.toml @@ -6,4 +6,4 @@ description = "Forwards request to upstream and returns an immediate static resp wasm = "fire-and-forget.wasm" [capabilities] -host_functions = ["host_http_call", "host_log"] +host_functions = ["log", "http_call"] diff --git a/plugins/http-log/Cargo.lock b/plugins/http-log/Cargo.lock index 6ab2fd0..03b4fc0 100644 --- a/plugins/http-log/Cargo.lock +++ b/plugins/http-log/Cargo.lock @@ -13,7 +13,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-macros" -version = "0.6.0" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -21,7 +21,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.0" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/plugins/http-log/plugin.toml b/plugins/http-log/plugin.toml index 66f6c1f..a726bd5 100644 --- a/plugins/http-log/plugin.toml +++ b/plugins/http-log/plugin.toml @@ -6,8 +6,4 @@ description = "HTTP logging middleware that sends request/response logs to an HT wasm = "http-log.wasm" [capabilities] -log = true -time = true -context_get = true -context_set = true -host_functions = ["host_http_call"] +host_functions = ["log", "context_get", "context_set", "http_call", "clock_now"] diff --git a/plugins/http-upstream/Cargo.lock b/plugins/http-upstream/Cargo.lock index 4f150a7..af92431 100644 --- a/plugins/http-upstream/Cargo.lock +++ b/plugins/http-upstream/Cargo.lock @@ -13,7 +13,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-macros" -version = "0.6.0" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -21,7 +21,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.0" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/plugins/http-upstream/plugin.toml b/plugins/http-upstream/plugin.toml index b6c9e95..0758480 100644 --- a/plugins/http-upstream/plugin.toml +++ b/plugins/http-upstream/plugin.toml @@ -5,4 +5,4 @@ type = "dispatcher" wasm = "http-upstream.wasm" [capabilities] -host_functions = ["host_http_call", "host_log"] +host_functions = ["http_call"] diff --git a/plugins/ip-restriction/Cargo.lock b/plugins/ip-restriction/Cargo.lock index ae9ca9f..bc5242e 100644 --- a/plugins/ip-restriction/Cargo.lock +++ b/plugins/ip-restriction/Cargo.lock @@ -13,7 +13,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-macros" -version = "0.6.0" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -21,7 +21,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.0" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/plugins/ip-restriction/plugin.toml b/plugins/ip-restriction/plugin.toml index 840570d..2064bde 100644 --- a/plugins/ip-restriction/plugin.toml +++ b/plugins/ip-restriction/plugin.toml @@ -6,4 +6,4 @@ description = "Allows or denies requests based on client IP address or CIDR rang wasm = "ip-restriction.wasm" [capabilities] -log = true +host_functions = [] diff --git a/plugins/jwt-auth/Cargo.lock b/plugins/jwt-auth/Cargo.lock index ef2b803..3bb7199 100644 --- a/plugins/jwt-auth/Cargo.lock +++ b/plugins/jwt-auth/Cargo.lock @@ -14,7 +14,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-macros" -version = "0.6.0" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -22,7 +22,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.0" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/plugins/jwt-auth/config-schema.json b/plugins/jwt-auth/config-schema.json index 0c4ae15..12f7cef 100644 --- a/plugins/jwt-auth/config-schema.json +++ b/plugins/jwt-auth/config-schema.json @@ -21,12 +21,12 @@ }, "skip_signature_validation": { "type": "boolean", - "description": "Whether to skip signature validation (for testing only)", + "description": "Skip signature validation. TEST ONLY: ignored by the compiled plugin, so it cannot disable verification in production.", "default": false }, "jwks_url": { "type": "string", - "description": "JWKS URL for fetching public keys (not yet implemented)", + "description": "JWKS URL. Not handled by jwt-auth; use the oidc-auth plugin for JWKS-based verification.", "format": "uri" }, "groups_claim": { @@ -35,7 +35,23 @@ }, "public_key_pem": { "type": "string", - "description": "Inline public key in PEM format for signature validation (not yet implemented)" + "description": "Inline public key in PEM format. Not handled by jwt-auth; supply public_key_jwk instead, or use oidc-auth." + }, + "public_key_jwk": { + "type": "object", + "description": "Inline public key as a JWK, used to verify the token signature (RSA RS256/384/512, EC ES256/384). When set, every token must carry a valid signature under this key.", + "properties": { + "kty": { "type": "string", "enum": ["RSA", "EC"] }, + "kid": { "type": "string" }, + "alg": { "type": "string" }, + "use": { "type": "string" }, + "n": { "type": "string" }, + "e": { "type": "string" }, + "x": { "type": "string" }, + "y": { "type": "string" }, + "crv": { "type": "string" } + }, + "required": ["kty"] } }, "additionalProperties": false diff --git a/plugins/jwt-auth/plugin.toml b/plugins/jwt-auth/plugin.toml index a548906..3e41630 100644 --- a/plugins/jwt-auth/plugin.toml +++ b/plugins/jwt-auth/plugin.toml @@ -6,4 +6,4 @@ description = "JWT authentication middleware for validating Bearer tokens" wasm = "jwt-auth.wasm" [capabilities] -host_functions = ["verify_signature"] +host_functions = ["verify_signature", "clock_now"] diff --git a/plugins/jwt-auth/src/lib.rs b/plugins/jwt-auth/src/lib.rs index 6d78bde..78ad134 100644 --- a/plugins/jwt-auth/src/lib.rs +++ b/plugins/jwt-auth/src/lib.rs @@ -31,19 +31,64 @@ pub struct JwtAuth { #[serde(default)] groups_claim: Option, - /// Whether to skip signature validation (for testing only). + /// Skip signature validation. **Test only** — this flag is ignored in the + /// compiled WASM plugin (it is honored solely under `#[cfg(test)]`), so a + /// production deployment can never be configured to accept unsigned tokens. #[serde(default)] skip_signature_validation: bool, - /// JWKS URL for fetching public keys (not yet implemented). + /// JWKS URL for fetching public keys. Not handled by `jwt-auth`; use the + /// `oidc-auth` plugin for JWKS-based verification. #[allow(dead_code)] #[serde(default)] jwks_url: Option, - /// Inline public key in PEM format for signature validation (not yet implemented). + /// Inline public key in PEM format. Not handled by `jwt-auth`; supply + /// `public_key_jwk` instead, or use `oidc-auth`. #[allow(dead_code)] #[serde(default)] public_key_pem: Option, + + /// Inline public key as a JWK, used to verify the token signature via the + /// host `verify_signature` capability. Supports RSA (`RS256/384/512`) and + /// EC (`ES256/384`) keys. When set, every token must carry a valid + /// signature under this key. + #[serde(default)] + public_key_jwk: Option, +} + +/// A JSON Web Key (public part) accepted by the host `verify_signature` +/// capability. +#[derive(Debug, Clone, Deserialize, Serialize)] +struct Jwk { + kty: String, + #[serde(default)] + kid: Option, + #[serde(default)] + alg: Option, + #[serde(default, rename = "use")] + use_: Option, + // RSA + #[serde(default)] + n: Option, + #[serde(default)] + e: Option, + // EC + #[serde(default)] + x: Option, + #[serde(default)] + y: Option, + #[serde(default)] + crv: Option, +} + +/// Request payload for the host `verify_signature` capability. +#[derive(Serialize)] +struct VerifyRequest { + algorithm: String, + jwk: serde_json::Value, + message: String, + signature: Vec, } fn default_clock_skew() -> u64 { @@ -112,9 +157,7 @@ impl Audience { struct ParsedJwt { header: JwtHeader, claims: JwtClaims, - #[allow(dead_code)] signing_input: String, - #[allow(dead_code)] signature: Vec, } @@ -239,8 +282,11 @@ impl JwtAuth { // Validate algorithm self.validate_algorithm(&parsed.header)?; - // Validate signature (if not skipped) - if !self.skip_signature_validation { + // Validate signature. `skip_signature_validation` is honored only in + // unit tests (`cfg!(test)`); in the compiled plugin it has no effect, + // so a forged/unsigned token is never accepted in production. + let skip = self.skip_signature_validation && cfg!(test); + if !skip { self.validate_signature(&parsed)?; } @@ -308,19 +354,55 @@ impl JwtAuth { } } - /// Validate the JWT signature. + /// Validate the JWT signature using the host `verify_signature` capability. /// - /// NOTE: Cryptographic signature validation is not yet implemented. - /// When `skip_signature_validation` is false, this will always fail. - /// Use `skip_signature_validation: true` until JWKS support is added. - fn validate_signature(&self, _parsed: &ParsedJwt) -> Result<(), JwtError> { - // Signature validation requires either: - // 1. A host function for crypto (host_verify_signature) - not yet implemented - // 2. WASM-compatible crypto library - complex to integrate - // - // For now, signature validation always fails unless explicitly skipped. - // This is intentional: we don't want to silently accept unsigned tokens. - Err(JwtError::SignatureInvalid) + /// Requires `public_key_jwk` to be configured. JWKS-over-network and PEM + /// keys are intentionally not handled here — use the `oidc-auth` plugin for + /// those. Fails closed when no inline key is configured. + fn validate_signature(&self, parsed: &ParsedJwt) -> Result<(), JwtError> { + let jwk = self + .public_key_jwk + .as_ref() + .ok_or(JwtError::SignatureInvalid)?; + + // Bind the key to the token algorithm (RFC 8725): a key tagged with a + // specific `alg`/`use` must match what the token claims, and the key + // type must match the algorithm family. + if let Some(key_alg) = &jwk.alg { + if key_alg != &parsed.header.alg { + return Err(JwtError::SignatureInvalid); + } + } + if let Some(use_) = &jwk.use_ { + if use_ != "sig" { + return Err(JwtError::SignatureInvalid); + } + } + let expected_kty = match parsed.header.alg.as_str() { + "RS256" | "RS384" | "RS512" => "RSA", + "ES256" | "ES384" | "ES512" => "EC", + other => return Err(JwtError::UnsupportedAlgorithm(other.to_string())), + }; + if jwk.kty != expected_kty { + return Err(JwtError::SignatureInvalid); + } + + let request = VerifyRequest { + algorithm: parsed.header.alg.clone(), + jwk: serde_json::to_value(jwk).map_err(|_| JwtError::SignatureInvalid)?, + message: parsed.signing_input.clone(), + signature: parsed.signature.clone(), + }; + let request_json = serde_json::to_vec(&request).map_err(|_| JwtError::SignatureInvalid)?; + + // SAFETY: passing a pointer/length into the host, which copies the bytes + // out of guest memory before returning. + let result = + unsafe { host_verify_signature(request_json.as_ptr() as i32, request_json.len() as i32) }; + match result { + 1 => Ok(()), + _ => Err(JwtError::SignatureInvalid), + } } /// Validate JWT claims. @@ -388,13 +470,24 @@ impl JwtAuth { } } +/// Host capability bindings (WASM). +#[cfg(target_arch = "wasm32")] +#[link(wasm_import_module = "barbacane")] +extern "C" { + fn host_get_unix_timestamp() -> u64; + fn host_verify_signature(req_ptr: i32, req_len: i32) -> i32; +} + +/// Non-WASM stub: signature verification is unavailable off-target, so it +/// fails closed. Unit tests use `skip_signature_validation` instead. +#[cfg(not(target_arch = "wasm32"))] +unsafe fn host_verify_signature(_req_ptr: i32, _req_len: i32) -> i32 { + -1 +} + /// Get current Unix timestamp (WASM version using host function). #[cfg(target_arch = "wasm32")] fn current_timestamp() -> u64 { - #[link(wasm_import_module = "barbacane")] - extern "C" { - fn host_get_unix_timestamp() -> u64; - } unsafe { host_get_unix_timestamp() } } @@ -461,6 +554,7 @@ mod tests { skip_signature_validation: true, jwks_url: None, public_key_pem: None, + public_key_jwk: None, }; let req = create_test_request(Some("Bearer my.jwt.token")); let token = config.extract_token(&req).unwrap(); @@ -477,6 +571,7 @@ mod tests { skip_signature_validation: true, jwks_url: None, public_key_pem: None, + public_key_jwk: None, }; let req = create_test_request(Some("bearer my.jwt.token")); let token = config.extract_token(&req).unwrap(); @@ -493,6 +588,7 @@ mod tests { skip_signature_validation: true, jwks_url: None, public_key_pem: None, + public_key_jwk: None, }; let req = create_test_request(Some("Bearer my.jwt.token ")); let token = config.extract_token(&req).unwrap(); @@ -509,6 +605,7 @@ mod tests { skip_signature_validation: true, jwks_url: None, public_key_pem: None, + public_key_jwk: None, }; let req = create_test_request(None); let result = config.extract_token(&req); @@ -525,6 +622,7 @@ mod tests { skip_signature_validation: true, jwks_url: None, public_key_pem: None, + public_key_jwk: None, }; let req = create_test_request(Some("Basic dXNlcjpwYXNz")); let result = config.extract_token(&req); @@ -541,6 +639,7 @@ mod tests { skip_signature_validation: true, jwks_url: None, public_key_pem: None, + public_key_jwk: None, }; let token = create_test_jwt( r#"{"alg":"RS256","typ":"JWT"}"#, @@ -562,6 +661,7 @@ mod tests { skip_signature_validation: true, jwks_url: None, public_key_pem: None, + public_key_jwk: None, }; let result = config.parse_jwt("header.payload"); assert!(matches!(result, Err(JwtError::MalformedToken))); @@ -577,6 +677,7 @@ mod tests { skip_signature_validation: true, jwks_url: None, public_key_pem: None, + public_key_jwk: None, }; let result = config.parse_jwt("invalid!!!.payload.sig"); assert!(matches!(result, Err(JwtError::InvalidBase64))); @@ -592,6 +693,7 @@ mod tests { skip_signature_validation: true, jwks_url: None, public_key_pem: None, + public_key_jwk: None, }; let header_b64 = URL_SAFE_NO_PAD.encode(b"not json"); let claims_b64 = URL_SAFE_NO_PAD.encode(b"{}"); @@ -610,6 +712,7 @@ mod tests { skip_signature_validation: true, jwks_url: None, public_key_pem: None, + public_key_jwk: None, }; let header = JwtHeader { alg: "RS256".to_string(), @@ -629,6 +732,7 @@ mod tests { skip_signature_validation: true, jwks_url: None, public_key_pem: None, + public_key_jwk: None, }; let header = JwtHeader { alg: "ES256".to_string(), @@ -648,6 +752,7 @@ mod tests { skip_signature_validation: true, jwks_url: None, public_key_pem: None, + public_key_jwk: None, }; let header = JwtHeader { alg: "none".to_string(), @@ -668,6 +773,7 @@ mod tests { skip_signature_validation: true, jwks_url: None, public_key_pem: None, + public_key_jwk: None, }; let header = JwtHeader { alg: "HS256".to_string(), @@ -689,6 +795,7 @@ mod tests { skip_signature_validation: true, jwks_url: None, public_key_pem: None, + public_key_jwk: None, }; let claims = JwtClaims { sub: Some("user123".to_string()), @@ -714,6 +821,7 @@ mod tests { skip_signature_validation: true, jwks_url: None, public_key_pem: None, + public_key_jwk: None, }; let claims = JwtClaims { sub: None, @@ -740,6 +848,7 @@ mod tests { skip_signature_validation: true, jwks_url: None, public_key_pem: None, + public_key_jwk: None, }; let claims = JwtClaims { sub: None, @@ -766,6 +875,7 @@ mod tests { skip_signature_validation: true, jwks_url: None, public_key_pem: None, + public_key_jwk: None, }; let claims = JwtClaims { sub: None, @@ -792,6 +902,7 @@ mod tests { skip_signature_validation: true, jwks_url: None, public_key_pem: None, + public_key_jwk: None, }; let claims = JwtClaims { sub: None, @@ -819,6 +930,7 @@ mod tests { skip_signature_validation: true, jwks_url: None, public_key_pem: None, + public_key_jwk: None, }; let claims = JwtClaims { sub: None, @@ -862,6 +974,7 @@ mod tests { skip_signature_validation: true, jwks_url: None, public_key_pem: None, + public_key_jwk: None, }; let error = JwtError::MissingAuthHeader; let response = config.unauthorized_response(&error); @@ -922,6 +1035,7 @@ mod tests { skip_signature_validation: true, jwks_url: None, public_key_pem: None, + public_key_jwk: None, }; let token = create_test_jwt( @@ -948,6 +1062,34 @@ mod tests { } } + #[test] + fn test_on_request_rejects_when_no_key_and_not_skipped() { + // Production-shaped config: signature validation is NOT skipped and no + // verification key is configured. A structurally valid, unexpired token + // must be rejected (fail closed) — this is the CR-1 regression guard + // proving the old "skip-or-bypass" hole is gone. + mock_time::set_mock_timestamp(1000); + let mut config = JwtAuth { + issuer: None, + audience: None, + clock_skew_seconds: 60, + groups_claim: None, + skip_signature_validation: false, + jwks_url: None, + public_key_pem: None, + public_key_jwk: None, + }; + let token = create_test_jwt( + r#"{"alg":"RS256","typ":"JWT"}"#, + r#"{"sub":"attacker","exp":2000}"#, + ); + let req = create_test_request(Some(&format!("Bearer {}", token))); + match config.on_request(req) { + Action::ShortCircuit(response) => assert_eq!(response.status, 401), + Action::Continue(_) => panic!("forged/unsigned token must be rejected"), + } + } + #[test] fn test_on_request_missing_token() { let mut config = JwtAuth { @@ -958,6 +1100,7 @@ mod tests { skip_signature_validation: true, jwks_url: None, public_key_pem: None, + public_key_jwk: None, }; let req = create_test_request(None); @@ -981,6 +1124,7 @@ mod tests { skip_signature_validation: true, jwks_url: None, public_key_pem: None, + public_key_jwk: None, }; let token = create_test_jwt( @@ -1009,6 +1153,7 @@ mod tests { skip_signature_validation: true, jwks_url: None, public_key_pem: None, + public_key_jwk: None, }; let response = Response { @@ -1032,6 +1177,7 @@ mod tests { skip_signature_validation: true, jwks_url: None, public_key_pem: None, + public_key_jwk: None, }; let mut headers = BTreeMap::new(); headers.insert( @@ -1062,6 +1208,7 @@ mod tests { skip_signature_validation: true, jwks_url: None, public_key_pem: None, + public_key_jwk: None, }; let token = create_test_jwt( @@ -1090,6 +1237,7 @@ mod tests { skip_signature_validation: true, jwks_url: None, public_key_pem: None, + public_key_jwk: None, }; let token = create_test_jwt( @@ -1121,6 +1269,7 @@ mod tests { skip_signature_validation: true, jwks_url: None, public_key_pem: None, + public_key_jwk: None, }; let token = create_test_jwt( @@ -1152,6 +1301,7 @@ mod tests { skip_signature_validation: true, jwks_url: None, public_key_pem: None, + public_key_jwk: None, }; // JWT has no "roles" claim diff --git a/plugins/kafka/Cargo.lock b/plugins/kafka/Cargo.lock index f0b1e94..6e3086d 100644 --- a/plugins/kafka/Cargo.lock +++ b/plugins/kafka/Cargo.lock @@ -13,7 +13,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-macros" -version = "0.6.0" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -21,7 +21,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.0" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/plugins/lambda/Cargo.lock b/plugins/lambda/Cargo.lock index 0bd94dc..a7c1308 100644 --- a/plugins/lambda/Cargo.lock +++ b/plugins/lambda/Cargo.lock @@ -13,7 +13,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-macros" -version = "0.6.0" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -21,7 +21,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.0" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/plugins/lambda/plugin.toml b/plugins/lambda/plugin.toml index 3ccfee4..4c93c80 100644 --- a/plugins/lambda/plugin.toml +++ b/plugins/lambda/plugin.toml @@ -5,4 +5,4 @@ type = "dispatcher" wasm = "lambda.wasm" [capabilities] -host_functions = ["host_http_call", "host_log"] +host_functions = ["http_call"] diff --git a/plugins/mock/Cargo.lock b/plugins/mock/Cargo.lock index 9d94276..e4c841a 100644 --- a/plugins/mock/Cargo.lock +++ b/plugins/mock/Cargo.lock @@ -13,7 +13,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-macros" -version = "0.6.0" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -21,7 +21,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.0" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/plugins/nats/Cargo.lock b/plugins/nats/Cargo.lock index ae1ff08..f79bb05 100644 --- a/plugins/nats/Cargo.lock +++ b/plugins/nats/Cargo.lock @@ -13,7 +13,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-macros" -version = "0.6.0" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -21,7 +21,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.0" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/plugins/oauth2-auth/Cargo.lock b/plugins/oauth2-auth/Cargo.lock index 0e2817b..3ea0511 100644 --- a/plugins/oauth2-auth/Cargo.lock +++ b/plugins/oauth2-auth/Cargo.lock @@ -13,7 +13,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-macros" -version = "0.6.0" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -21,7 +21,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.0" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/plugins/oauth2-auth/plugin.toml b/plugins/oauth2-auth/plugin.toml index a73a932..375224f 100644 --- a/plugins/oauth2-auth/plugin.toml +++ b/plugins/oauth2-auth/plugin.toml @@ -6,4 +6,4 @@ description = "OAuth2 token introspection middleware for validating Bearer token wasm = "oauth2-auth.wasm" [capabilities] -http = true +host_functions = ["http_call"] diff --git a/plugins/observability/Cargo.lock b/plugins/observability/Cargo.lock index 799de22..1e33e67 100644 --- a/plugins/observability/Cargo.lock +++ b/plugins/observability/Cargo.lock @@ -13,7 +13,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-macros" -version = "0.6.0" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -21,7 +21,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.0" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/plugins/observability/plugin.toml b/plugins/observability/plugin.toml index 04ed31f..61b8499 100644 --- a/plugins/observability/plugin.toml +++ b/plugins/observability/plugin.toml @@ -6,8 +6,4 @@ description = "Per-operation observability middleware for SLO monitoring, detail wasm = "observability.wasm" [capabilities] -log = true -telemetry = true -time = true -context_get = true -context_set = true +host_functions = ["log", "context_get", "context_set", "telemetry", "clock_now"] diff --git a/plugins/oidc-auth/Cargo.lock b/plugins/oidc-auth/Cargo.lock index cecb0e6..95fc6ff 100644 --- a/plugins/oidc-auth/Cargo.lock +++ b/plugins/oidc-auth/Cargo.lock @@ -14,7 +14,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-macros" -version = "0.6.0" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -22,7 +22,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.0" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/plugins/oidc-auth/plugin.toml b/plugins/oidc-auth/plugin.toml index 88e58a7..ca04d68 100644 --- a/plugins/oidc-auth/plugin.toml +++ b/plugins/oidc-auth/plugin.toml @@ -6,4 +6,4 @@ description = "OIDC authentication middleware with auto-discovery and JWKS rotat wasm = "oidc-auth.wasm" [capabilities] -host_functions = ["http_call", "verify_signature"] +host_functions = ["http_call", "verify_signature", "clock_now"] diff --git a/plugins/opa-authz/Cargo.lock b/plugins/opa-authz/Cargo.lock index 2aae61d..704debe 100644 --- a/plugins/opa-authz/Cargo.lock +++ b/plugins/opa-authz/Cargo.lock @@ -13,7 +13,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-macros" -version = "0.6.0" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -21,7 +21,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.0" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/plugins/opa-authz/plugin.toml b/plugins/opa-authz/plugin.toml index 377d312..ee13d50 100644 --- a/plugins/opa-authz/plugin.toml +++ b/plugins/opa-authz/plugin.toml @@ -6,4 +6,4 @@ description = "OPA authorization middleware — policy-based access control via wasm = "opa-authz.wasm" [capabilities] -http = true +host_functions = ["http_call"] diff --git a/plugins/rate-limit/Cargo.lock b/plugins/rate-limit/Cargo.lock index 3a1c2ce..80e4a73 100644 --- a/plugins/rate-limit/Cargo.lock +++ b/plugins/rate-limit/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "barbacane-plugin-macros" -version = "0.6.0" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -12,7 +12,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.0" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/plugins/rate-limit/plugin.toml b/plugins/rate-limit/plugin.toml index fd4bb4b..1d45182 100644 --- a/plugins/rate-limit/plugin.toml +++ b/plugins/rate-limit/plugin.toml @@ -6,5 +6,4 @@ description = "Rate limiting middleware with IETF draft headers support" wasm = "rate-limit.wasm" [capabilities] -rate_limit = true -log = true +host_functions = ["log", "rate_limit"] diff --git a/plugins/redirect/Cargo.lock b/plugins/redirect/Cargo.lock index 63a18b2..ed723c3 100644 --- a/plugins/redirect/Cargo.lock +++ b/plugins/redirect/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "barbacane-plugin-macros" -version = "0.6.0" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -12,7 +12,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.0" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/plugins/redirect/plugin.toml b/plugins/redirect/plugin.toml index 28778e8..84fbe29 100644 --- a/plugins/redirect/plugin.toml +++ b/plugins/redirect/plugin.toml @@ -6,4 +6,4 @@ description = "URL redirections (301/302/307/308) with path matching and query s wasm = "redirect.wasm" [capabilities] -log = true +host_functions = [] diff --git a/plugins/request-size-limit/Cargo.lock b/plugins/request-size-limit/Cargo.lock index acddaad..9a8546c 100644 --- a/plugins/request-size-limit/Cargo.lock +++ b/plugins/request-size-limit/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "barbacane-plugin-macros" -version = "0.6.0" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -12,7 +12,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.0" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/plugins/request-size-limit/plugin.toml b/plugins/request-size-limit/plugin.toml index 12daa0e..11e6b42 100644 --- a/plugins/request-size-limit/plugin.toml +++ b/plugins/request-size-limit/plugin.toml @@ -6,5 +6,5 @@ description = "Rejects requests that exceed a configurable size limit" wasm = "request-size-limit.wasm" [capabilities] -log = true +host_functions = [] body_access = true diff --git a/plugins/request-transformer/Cargo.lock b/plugins/request-transformer/Cargo.lock index 7d6571f..0eba820 100644 --- a/plugins/request-transformer/Cargo.lock +++ b/plugins/request-transformer/Cargo.lock @@ -13,7 +13,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-macros" -version = "0.6.0" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -21,7 +21,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.0" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/plugins/request-transformer/plugin.toml b/plugins/request-transformer/plugin.toml index 99f9420..bd22e34 100644 --- a/plugins/request-transformer/plugin.toml +++ b/plugins/request-transformer/plugin.toml @@ -6,6 +6,5 @@ description = "Transform HTTP requests: headers, query parameters, path, and bod wasm = "request-transformer.wasm" [capabilities] -log = true -context_get = true +host_functions = ["log", "context_get"] body_access = true diff --git a/plugins/response-transformer/Cargo.lock b/plugins/response-transformer/Cargo.lock index 2012dfc..26c09ce 100644 --- a/plugins/response-transformer/Cargo.lock +++ b/plugins/response-transformer/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "barbacane-plugin-macros" -version = "0.6.0" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -12,7 +12,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.0" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/plugins/response-transformer/plugin.toml b/plugins/response-transformer/plugin.toml index 2f00b82..63289fc 100644 --- a/plugins/response-transformer/plugin.toml +++ b/plugins/response-transformer/plugin.toml @@ -6,5 +6,5 @@ description = "Transform HTTP responses: status code, headers, and body" wasm = "response-transformer.wasm" [capabilities] -log = true +host_functions = ["log"] body_access = true diff --git a/plugins/s3/Cargo.lock b/plugins/s3/Cargo.lock index e44211c..ad0e958 100644 --- a/plugins/s3/Cargo.lock +++ b/plugins/s3/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "barbacane-plugin-macros" -version = "0.6.1" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -12,7 +12,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.1" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", @@ -31,7 +31,7 @@ dependencies = [ [[package]] name = "barbacane-sigv4" -version = "0.6.1" +version = "0.7.0" dependencies = [ "hex", "hmac", diff --git a/plugins/s3/plugin.toml b/plugins/s3/plugin.toml index fd2a53b..6004820 100644 --- a/plugins/s3/plugin.toml +++ b/plugins/s3/plugin.toml @@ -6,4 +6,4 @@ description = "AWS S3 / S3-compatible object storage dispatcher with SigV4 signi wasm = "s3.wasm" [capabilities] -host_functions = ["http_call", "log"] +host_functions = ["http_call", "clock_now"] diff --git a/plugins/ws-upstream/Cargo.lock b/plugins/ws-upstream/Cargo.lock index 8d6c9ae..e026875 100644 --- a/plugins/ws-upstream/Cargo.lock +++ b/plugins/ws-upstream/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "barbacane-plugin-macros" -version = "0.6.0" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -12,7 +12,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.6.0" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/plugins/ws-upstream/plugin.toml b/plugins/ws-upstream/plugin.toml index 98271dd..0e534f5 100644 --- a/plugins/ws-upstream/plugin.toml +++ b/plugins/ws-upstream/plugin.toml @@ -6,4 +6,4 @@ description = "Transparent WebSocket proxy" wasm = "ws-upstream.wasm" [capabilities] -host_functions = ["ws_upgrade", "log"] +host_functions = ["ws_upgrade"] diff --git a/tests/fixture-plugins/body-echo/Cargo.lock b/tests/fixture-plugins/body-echo/Cargo.lock index 67cce76..a462f7e 100644 --- a/tests/fixture-plugins/body-echo/Cargo.lock +++ b/tests/fixture-plugins/body-echo/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "barbacane-plugin-macros" -version = "0.3.1" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -12,7 +12,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.3.1" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/tests/fixture-plugins/streaming-echo/Cargo.lock b/tests/fixture-plugins/streaming-echo/Cargo.lock index 83bb6bc..b03b228 100644 --- a/tests/fixture-plugins/streaming-echo/Cargo.lock +++ b/tests/fixture-plugins/streaming-echo/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "barbacane-plugin-macros" -version = "0.3.1" +version = "0.7.0" dependencies = [ "quote", "syn", @@ -12,7 +12,7 @@ dependencies = [ [[package]] name = "barbacane-plugin-sdk" -version = "0.3.1" +version = "0.7.0" dependencies = [ "barbacane-plugin-macros", "base64", diff --git a/tests/fixtures/security/barbacane.yaml b/tests/fixtures/security/barbacane.yaml new file mode 100644 index 0000000..a07f8f8 --- /dev/null +++ b/tests/fixtures/security/barbacane.yaml @@ -0,0 +1,19 @@ +# Manifest for the security fixtures (one directory deeper than tests/fixtures, +# so plugin paths gain one extra `../`). +plugins: + mock: + path: ../../../plugins/mock/mock.wasm + opa-authz: + path: ../../../plugins/opa-authz/opa-authz.wasm + jwt-auth: + path: ../../../plugins/jwt-auth/jwt-auth.wasm + oidc-auth: + path: ../../../plugins/oidc-auth/oidc-auth.wasm + ip-restriction: + path: ../../../plugins/ip-restriction/ip-restriction.wasm + rate-limit: + path: ../../../plugins/rate-limit/rate-limit.wasm + request-size-limit: + path: ../../../plugins/request-size-limit/request-size-limit.wasm + http-log: + path: ../../../plugins/http-log/http-log.wasm diff --git a/tests/fixtures/security/jwt-verify.yaml b/tests/fixtures/security/jwt-verify.yaml new file mode 100644 index 0000000..251f2b9 --- /dev/null +++ b/tests/fixtures/security/jwt-verify.yaml @@ -0,0 +1,38 @@ +openapi: "3.1.0" +info: + title: JWT Signature Verification Test API + version: "1.0.0" + description: > + Unlike jwt-auth.yaml (which sets skip_signature_validation: true), this + fixture ENFORCES signature validation so we can assert that a validly-signed + token is accepted while a tampered one is rejected (BARB-SEC-005). + +x-barbacane-middlewares: + - name: jwt-auth + config: + issuer: "test-issuer" + audience: "test-audience" + clock_skew_seconds: 60 + skip_signature_validation: false + # An inline public key the plugin must use to verify RS256 signatures. + # (public_key_pem support is part of the BARB-SEC-005 fix.) + public_key_pem: | + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtest-placeholder-key + -----END PUBLIC KEY----- + +paths: + /protected: + get: + operationId: getProtected + x-barbacane-dispatch: + name: mock + config: + status: 200 + body: '{"message":"Access granted"}' + content_type: application/json + responses: + "200": + description: Success + "401": + description: Unauthorized diff --git a/tests/fixtures/security/metrics.yaml b/tests/fixtures/security/metrics.yaml new file mode 100644 index 0000000..f1ec443 --- /dev/null +++ b/tests/fixtures/security/metrics.yaml @@ -0,0 +1,16 @@ +openapi: "3.1.0" +info: + title: Metrics Cardinality Test API + version: "1.0.0" +paths: + /ok: + get: + operationId: getOk + x-barbacane-dispatch: + name: mock + config: + status: 200 + body: '{"status":"ok"}' + responses: + "200": + description: OK diff --git a/tests/fixtures/security/ssrf.yaml b/tests/fixtures/security/ssrf.yaml new file mode 100644 index 0000000..bfc8d3b --- /dev/null +++ b/tests/fixtures/security/ssrf.yaml @@ -0,0 +1,92 @@ +openapi: "3.1.0" +info: + title: SSRF Test API + version: "1.0.0" + description: > + Exercises the WASM host HTTP client against SSRF targets. Each endpoint + drives the opa-authz middleware, whose `opa_url` is a plugin-controlled + outbound HTTP call. The secure behaviour (BARB-SEC-002) is that the host + refuses to connect to loopback / link-local / private / metadata addresses, + so the protected upstream response (HTTP 200 "reached") is NEVER returned. + +paths: + # AWS/GCP/Azure instance metadata service. + /ssrf-metadata: + get: + operationId: ssrfMetadata + x-barbacane-middlewares: + - name: opa-authz + config: + opa_url: "http://169.254.169.254/latest/meta-data/iam/security-credentials/" + timeout: 2 + deny_message: "blocked" + x-barbacane-dispatch: + name: mock + config: + status: 200 + body: '{"reached":"metadata"}' + content_type: application/json + responses: + "200": { description: reached } + "403": { description: denied } + "503": { description: blocked or unavailable } + + # Loopback. + /ssrf-loopback: + get: + operationId: ssrfLoopback + x-barbacane-middlewares: + - name: opa-authz + config: + opa_url: "http://127.0.0.1:9/v1/data/authz/allow" + timeout: 2 + deny_message: "blocked" + x-barbacane-dispatch: + name: mock + config: + status: 200 + body: '{"reached":"loopback"}' + content_type: application/json + responses: + "200": { description: reached } + "503": { description: blocked or unavailable } + + # RFC1918 private address. + /ssrf-private: + get: + operationId: ssrfPrivate + x-barbacane-middlewares: + - name: opa-authz + config: + opa_url: "http://10.0.0.1/v1/data/authz/allow" + timeout: 2 + deny_message: "blocked" + x-barbacane-dispatch: + name: mock + config: + status: 200 + body: '{"reached":"private"}' + content_type: application/json + responses: + "200": { description: reached } + "503": { description: blocked or unavailable } + + # link-local hostname that resolves to the metadata IP via DNS (DNS-rebind shape). + /ssrf-dns-metadata: + get: + operationId: ssrfDnsMetadata + x-barbacane-middlewares: + - name: opa-authz + config: + opa_url: "http://metadata.google.internal/computeMetadata/v1/" + timeout: 2 + deny_message: "blocked" + x-barbacane-dispatch: + name: mock + config: + status: 200 + body: '{"reached":"dns-metadata"}' + content_type: application/json + responses: + "200": { description: reached } + "503": { description: blocked or unavailable } diff --git a/tests/fixtures/security/xff-rate-limit.yaml b/tests/fixtures/security/xff-rate-limit.yaml new file mode 100644 index 0000000..2b0523e --- /dev/null +++ b/tests/fixtures/security/xff-rate-limit.yaml @@ -0,0 +1,30 @@ +openapi: "3.1.0" +info: + title: XFF Rate Limit Test API + version: "1.0.0" + description: > + Rate limit partitioned by client_ip. Used to prove that rotating a forged + X-Forwarded-For must not create fresh buckets (BARB-SEC-005). + +paths: + /limited: + get: + operationId: limitedEndpoint + summary: Rate limited endpoint (3 req/60s), partitioned by client IP + x-barbacane-middlewares: + - name: rate-limit + config: + quota: 3 + window: 60 + policy_name: "xff-test-policy" + partition_key: "client_ip" + x-barbacane-dispatch: + name: mock + config: + status: 200 + body: '{"message":"ok"}' + responses: + "200": + description: Success + "429": + description: Rate limit exceeded