Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions crates/buzz-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,17 @@ buzz-ws-client = { path = "../buzz-ws-client" }
# Random number generation — full jitter for exponential backoff in with_retry
rand = { workspace = true }

# Zeroizes the private key string in memory on drop — defense in depth for
# --private-key / --private-key-fd secret handling
zeroize = "1"

[target.'cfg(unix)'.dependencies]
# Closes the raw inherited fd after reading `--private-key-fd` — opening
# `/dev/fd/<fd>` duplicates the descriptor, so the original must be closed
# explicitly or it leaks for the process lifetime. `unistd::close` is
# unconditionally available, so no extra feature flags are needed.
nix = { version = "0.31", default-features = false }

[dev-dependencies]
# Scratch files for channel-templates.json fixtures in tests
tempfile = "3"
Expand Down
8 changes: 8 additions & 0 deletions crates/buzz-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ export BUZZ_PRIVATE_KEY="nsec1..."
buzz channels list
```

For handoff scenarios where the key shouldn't appear in argv, shell history,
or the environment (e.g. a parent process/harness spawning `buzz`), pass
`--private-key-fd <FD>` instead: the CLI reads the key from the inherited
file descriptor `/dev/fd/<FD>` (fd must be >= 3; Linux/macOS only).
`--private-key-fd` and `--private-key`/`BUZZ_PRIVATE_KEY` are mutually
exclusive.

## Usage

All output is JSON on stdout. Errors are JSON on stderr. Exit codes: 0=ok, 1=user error, 2=network, 3=auth, 4=other, 5=write conflict.
Expand Down Expand Up @@ -156,6 +163,7 @@ stored rules in `validation_error` so an owner can remove and repair them.
| `upload` | `file` | Upload a file to the Blossom store |
| `pack` | `validate` | Validate a persona pack (local, no relay) |
| | `inspect` | Inspect a persona pack (local, no relay) |
| `capabilities` | | Report security capabilities as versioned JSON (local, no relay, no private key) |
| `mem` | `ls` | List non-tombstoned memories |
| | `get` | Print memory value to stdout |
| | `hash` | Print SHA-256 hex of memory value |
Expand Down
15 changes: 13 additions & 2 deletions crates/buzz-cli/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,7 @@ buzz users set-profile 2>&1; echo "exit: $?"
# Exit 3: No auth configured
env -u BUZZ_PRIVATE_KEY \
cargo run -p buzz-cli -- channels list 2>&1; echo "exit: $?"
# stderr: {"error":"auth_error","message":"auth error: BUZZ_PRIVATE_KEY is required (use --private-key or set env var)"}
# stderr: {"error":"auth_error","message":"auth error: BUZZ_PRIVATE_KEY is required (use --private-key, --private-key-fd, or set env var)"}
# exit: 3

# Not-found returns null, not an error (exit 0)
Expand All @@ -522,10 +522,20 @@ Test authentication.
BUZZ_PRIVATE_KEY="nsec1..." buzz channels list | jq .
# Should succeed

# Private key via fd (--private-key-fd)
exec 3< <(printf '%s' "nsec1...")
buzz --private-key-fd 3 channels list | jq .
exec 3<&-
# Should succeed

# --private-key and --private-key-fd together → clap usage error
buzz --private-key "nsec1..." --private-key-fd 3 channels list 2>&1; echo "exit: $?"
# exit: 1 (clap conflicts_with error)

# No auth → exit 3
env -u BUZZ_PRIVATE_KEY \
cargo run -p buzz-cli -- channels list 2>&1; echo "exit: $?"
# stderr: {"error":"auth_error","message":"auth error: BUZZ_PRIVATE_KEY is required (use --private-key or set env var)"}
# stderr: {"error":"auth_error","message":"auth error: BUZZ_PRIVATE_KEY is required (use --private-key, --private-key-fd, or set env var)"}
# exit: 3
```

Expand Down Expand Up @@ -606,3 +616,4 @@ buzz channels delete --channel "$FORUM_ID" | jq .
| 59 | `notes get` | ☐ | By name, by naddr, --content-only, cross-author, ambiguous → exit 1 |
| 60 | `notes ls` | ☐ | Own, --author all, --tag, --limit |
| 61 | `notes rm` | ☐ | Delete→get 404, double-delete idempotent, missing slug → NotFound |
| 62 | `capabilities` | ☐ | Local, no relay, no private key; versioned JSON security-capability report |
175 changes: 175 additions & 0 deletions crates/buzz-cli/src/commands/capabilities.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
//! `buzz capabilities` — machine-readable security-contract report.
//!
//! This command runs entirely locally: it makes no relay connection, reads no
//! private key material, and does not inspect `BUZZ_PRIVATE_KEY`,
//! `BUZZ_AUTH_TAG`, or any other environment variable. It exists so that
//! downstream automated verifiers (e.g. a "doctor" tool checking that a given
//! `buzz` build actually implements the `--private-key-fd` security
//! properties) have something more reliable to check than grepping
//! `--help` text.
//!
//! The emitted JSON is a **stable, versioned attestation contract** for
//! downstream tooling:
//!
//! - `schema_version` identifies the *shape* of the object. Bump it (and
//! never repurpose or remove an existing field's meaning) if the shape of
//! this JSON changes — additive-only evolution should still bump it so
//! strict downstream parsers can detect the change.
//! - `capability_revision` identifies the specific implementation revision of
//! the `--private-key-fd` feature being attested to
//! (`secure_private_key_fd_v1`). If the underlying security properties
//! change (e.g. a guarantee is weakened, removed, or a new one is added),
//! mint a new revision string rather than silently redefining what the old
//! one means.

use serde::Serialize;

use crate::error::CliError;
use crate::{MAX_PRIVATE_KEY_FD, MIN_PRIVATE_KEY_FD, PRIVATE_KEY_FD_MAX_LEN};

/// `schema_version` of the JSON object emitted by [`cmd_show`].
const SCHEMA_VERSION: u32 = 1;

/// Stable identifier for the exact set of `--private-key-fd` security
/// properties this report attests to. Only bump this if the contract's
/// meaning changes (e.g. one of the guarantees below is weakened, removed, or
/// a new one is added) — not on every unrelated code change.
const CAPABILITY_REVISION: &str = "secure_private_key_fd_v1";

/// Whether `--private-key-fd` is actually usable on this build. This must
/// fail closed: it is only `true` when compiled for Unix, where
/// [`crate::read_key_from_fd`] has a real `/dev/fd`-backed implementation. On
/// any other target (e.g. Windows) the flag is parsed by clap but the
/// underlying read always errors out, so this reports `false` rather than
/// claiming a working feature.
#[cfg(unix)]
const PRIVATE_KEY_FD_SUPPORTED: bool = true;

#[cfg(not(unix))]
const PRIVATE_KEY_FD_SUPPORTED: bool = false;

/// Security-relevant details of the `--private-key-fd` implementation.
///
/// All fields other than `supported` describe the implementation as it
/// exists in the Unix build and stay fixed regardless of target platform —
/// `supported` alone is what tells a downstream consumer whether this
/// platform's build actually backs those properties with a real
/// implementation.
#[derive(Serialize)]
struct PrivateKeyFdCapabilities {
/// `true` only on Unix targets, where `--private-key-fd` has a real
/// `/dev/fd`-backed implementation. Fails closed on other platforms.
supported: bool,
/// How the key material is handed to the process.
transport: &'static str,
/// Minimum accepted file descriptor number (inclusive).
min_fd: u32,
/// Maximum accepted file descriptor number (inclusive).
max_fd: u32,
/// Maximum number of bytes read from the fd for the key.
max_key_bytes: usize,
/// Whether the original fd passed via `--private-key-fd` is closed after
/// being read.
closes_original_fd: bool,
/// Whether the in-memory buffer holding the key is zeroized.
zeroizes_input: bool,
/// Whether `--help` hides the live `BUZZ_PRIVATE_KEY` env value.
help_env_value_redacted: bool,
}

/// Top-level `buzz capabilities` report.
#[derive(Serialize)]
struct CapabilitiesReport {
/// Shape version of this JSON object. Bump on any shape change.
schema_version: u32,
/// Implementation revision identifier for the `--private-key-fd`
/// security properties attested to below.
capability_revision: &'static str,
private_key_fd: PrivateKeyFdCapabilities,
}

impl Default for CapabilitiesReport {
fn default() -> Self {
Self {
schema_version: SCHEMA_VERSION,
capability_revision: CAPABILITY_REVISION,
private_key_fd: PrivateKeyFdCapabilities {
supported: PRIVATE_KEY_FD_SUPPORTED,
transport: "inherited_fd",
min_fd: MIN_PRIVATE_KEY_FD,
max_fd: MAX_PRIVATE_KEY_FD,
max_key_bytes: PRIVATE_KEY_FD_MAX_LEN,
closes_original_fd: true,
zeroizes_input: true,
help_env_value_redacted: true,
},
}
}
}

/// Run `buzz capabilities`.
///
/// Prints one JSON object to stdout describing the security properties of
/// the `--private-key-fd` implementation and returns `Ok(())` (exit code 0).
/// Reads no environment variables and performs no I/O beyond stdout.
pub fn cmd_show() -> Result<(), CliError> {
let report = CapabilitiesReport::default();
let json = serde_json::to_string(&report)
.map_err(|e| CliError::Other(format!("failed to serialize capabilities report: {e}")))?;
println!("{json}");
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn schema_version_and_revision_are_stable() {
assert_eq!(SCHEMA_VERSION, 1);
assert_eq!(CAPABILITY_REVISION, "secure_private_key_fd_v1");
}

#[test]
fn private_key_fd_supported_matches_platform() {
// Fails closed: only true on Unix, where read_key_from_fd has a real
// /dev/fd-backed implementation.
assert_eq!(PRIVATE_KEY_FD_SUPPORTED, cfg!(unix));
}

#[test]
fn cmd_show_succeeds() {
assert!(cmd_show().is_ok());
}

#[test]
fn report_matches_expected_contract() {
let report = CapabilitiesReport::default();
assert_eq!(report.schema_version, 1);
assert_eq!(report.capability_revision, "secure_private_key_fd_v1");
assert_eq!(report.private_key_fd.transport, "inherited_fd");
assert_eq!(report.private_key_fd.min_fd, 3);
assert_eq!(report.private_key_fd.max_fd, 1024);
assert_eq!(report.private_key_fd.max_key_bytes, 256);
assert!(report.private_key_fd.closes_original_fd);
assert!(report.private_key_fd.zeroizes_input);
assert!(report.private_key_fd.help_env_value_redacted);
assert_eq!(report.private_key_fd.supported, cfg!(unix));
}

#[test]
fn serializes_to_expected_json_shape() {
let report = CapabilitiesReport::default();
let value: serde_json::Value = serde_json::to_value(&report).unwrap();
assert_eq!(value["schema_version"], 1);
assert_eq!(value["capability_revision"], "secure_private_key_fd_v1");
assert_eq!(value["private_key_fd"]["transport"], "inherited_fd");
assert_eq!(value["private_key_fd"]["min_fd"], 3);
assert_eq!(value["private_key_fd"]["max_fd"], 1024);
assert_eq!(value["private_key_fd"]["max_key_bytes"], 256);
assert_eq!(value["private_key_fd"]["closes_original_fd"], true);
assert_eq!(value["private_key_fd"]["zeroizes_input"], true);
assert_eq!(value["private_key_fd"]["help_env_value_redacted"], true);
assert_eq!(value["private_key_fd"]["supported"], cfg!(unix));
}
}
1 change: 1 addition & 0 deletions crates/buzz-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod agents;
pub mod capabilities;
pub mod channel_templates;
pub mod channels;
pub mod dms;
Expand Down
Loading