diff --git a/.github/workflows/cargo-deny.yml b/.github/workflows/cargo-deny.yml index 16e2799b..9e943f34 100644 --- a/.github/workflows/cargo-deny.yml +++ b/.github/workflows/cargo-deny.yml @@ -26,10 +26,7 @@ jobs: - uses: actions/checkout@v4 - uses: ./.github/actions/setup-rust - name: Install cargo-deny - run: | - if ! cargo deny --version 2>/dev/null | grep -q "0.19"; then - cargo install cargo-deny --locked - fi + uses: taiki-e/install-action@cargo-deny - name: cargo deny check ${{ matrix.checks }} run: cargo deny --manifest-path ./Cargo.toml check ${{ matrix.checks }} diff --git a/Cargo.lock b/Cargo.lock index 1a04970f..74c6691b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2460,9 +2460,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.18" +version = "0.9.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" dependencies = [ "crossbeam-utils", ] @@ -2478,9 +2478,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] name = "crunchy" diff --git a/crates/auths-cli/src/cli.rs b/crates/auths-cli/src/cli.rs index 4365ffcc..d1f04500 100644 --- a/crates/auths-cli/src/cli.rs +++ b/crates/auths-cli/src/cli.rs @@ -26,9 +26,9 @@ use crate::commands::error_lookup::ErrorLookupCommand; use crate::commands::id::IdCommand; use crate::commands::init::InitCommand; use crate::commands::ipex::IpexCommand; +use crate::commands::kel::KelCommand; use crate::commands::keri_emit::KeriEmitCommand; use crate::commands::key::KeyCommand; -use crate::commands::kel::KelCommand; use crate::commands::key_state::KeyStateCommand; use crate::commands::learn::LearnCommand; use crate::commands::log::LogCommand; diff --git a/crates/auths-cli/src/commands/doctor.rs b/crates/auths-cli/src/commands/doctor.rs index 475d8cd6..7dcf8c13 100644 --- a/crates/auths-cli/src/commands/doctor.rs +++ b/crates/auths-cli/src/commands/doctor.rs @@ -439,9 +439,7 @@ fn check_keychain_accessible() -> Check { /// `~/.auths` default. Surfacing the winner and its source turns "why can't it /// find my identity" into a one-glance answer instead of a guessing game. fn check_identity_home() -> Check { - let env_home = std::env::var("AUTHS_HOME") - .ok() - .filter(|s| !s.is_empty()); + let env_home = std::env::var("AUTHS_HOME").ok().filter(|s| !s.is_empty()); let (passed, detail, suggestion) = match auths_sdk::paths::auths_home() { Ok(path) => { let source = if env_home.is_some() { diff --git a/crates/auths-cli/src/commands/id/identity.rs b/crates/auths-cli/src/commands/id/identity.rs index 2d28c647..c5d37c53 100644 --- a/crates/auths-cli/src/commands/id/identity.rs +++ b/crates/auths-cli/src/commands/id/identity.rs @@ -251,7 +251,10 @@ pub enum IdSubcommand { /// Principal to bind the key to (usually the committer email). Defaults /// to the identity DID. Must match the signer identity git checks. - #[arg(long, help = "Principal for the allowed_signers line (default: identity DID)")] + #[arg( + long, + help = "Principal for the allowed_signers line (default: identity DID)" + )] principal: Option, /// Write to this file instead of stdout. @@ -911,13 +914,18 @@ pub fn handle_id( // allowed_signers line: ` `. The OpenSSH // encoding already carries ` `; trim any trailing // comment so the line is exactly the two fields ssh expects. - let key_field = openssh.split_whitespace().take(2).collect::>().join(" "); + let key_field = openssh + .split_whitespace() + .take(2) + .collect::>() + .join(" "); let line = format!("{principal} {key_field}\n"); match &output_file { Some(path) => { - fs::write(path, &line) - .with_context(|| format!("Failed to write allowed_signers to {:?}", path))?; + fs::write(path, &line).with_context(|| { + format!("Failed to write allowed_signers to {:?}", path) + })?; println!("✅ Wrote allowed_signers for {} to {:?}", principal, path); println!(" git config gpg.ssh.allowedSignersFile {:?}", path); } diff --git a/crates/auths-cli/src/commands/kel.rs b/crates/auths-cli/src/commands/kel.rs index 293cf85f..d3529dfe 100644 --- a/crates/auths-cli/src/commands/kel.rs +++ b/crates/auths-cli/src/commands/kel.rs @@ -112,19 +112,14 @@ fn load_events(args: &KelValidateArgs, ctx: &CliConfig) -> Result<(String, Vec d.clone(), None => { - let sdk_ctx = crate::factories::storage::build_auths_context( - &auths_home, - &ctx.env_config, - None, - )?; + let sdk_ctx = + crate::factories::storage::build_auths_context(&auths_home, &ctx.env_config, None)?; auths_sdk::workflows::commit_trust::local_self_root(&sdk_ctx).ok_or_else(|| { - anyhow!( - "no local identity found — run `auths init` first, or pass --kel/--did" - ) + anyhow!("no local identity found — run `auths init` first, or pass --kel/--did") })? } }; diff --git a/crates/auths-cli/src/commands/mod.rs b/crates/auths-cli/src/commands/mod.rs index 01680926..dd2bfa27 100644 --- a/crates/auths-cli/src/commands/mod.rs +++ b/crates/auths-cli/src/commands/mod.rs @@ -26,10 +26,10 @@ pub mod id; pub mod index; pub mod init; pub mod ipex; +pub mod kel; pub mod keri_emit; pub mod key; pub mod key_detect; -pub mod kel; pub mod key_state; pub mod learn; pub mod log; diff --git a/crates/auths-cli/src/ux/dialogs.rs b/crates/auths-cli/src/ux/dialogs.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/crates/auths-cli/src/ux/mod.rs b/crates/auths-cli/src/ux/mod.rs index 6188baea..e37b4b87 100644 --- a/crates/auths-cli/src/ux/mod.rs +++ b/crates/auths-cli/src/ux/mod.rs @@ -1,4 +1,3 @@ -pub mod dialogs; pub mod format; pub mod ident; diff --git a/crates/auths-infra-http/src/pairing_client.rs b/crates/auths-infra-http/src/pairing_client.rs index 5a13067c..abfc0eea 100644 --- a/crates/auths-infra-http/src/pairing_client.rs +++ b/crates/auths-infra-http/src/pairing_client.rs @@ -125,7 +125,8 @@ impl PairingRelayClient for HttpPairingRelayClient { // Prove knowledge of the short code without sending it in the clear: HMAC // a canonical GET with a key derived from the code. The timestamp and a // fresh random nonce bind the request against replay. - #[allow(clippy::disallowed_methods)] // wire boundary: no clock is injected into the relay client + #[allow(clippy::disallowed_methods)] + // wire boundary: no clock is injected into the relay client let ts = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_secs() as i64) @@ -136,10 +137,10 @@ impl PairingRelayClient for HttpPairingRelayClient { let mut rng = rand::rngs::OsRng; rng.fill_bytes(&mut nonce); } - let req = self - .client - .get(&url) - .header("Authorization", build_lookup_authorization(code, ts, &nonce)); + let req = self.client.get(&url).header( + "Authorization", + build_lookup_authorization(code, ts, &nonce), + ); async move { guard?; diff --git a/crates/auths-pairing-protocol/src/lib.rs b/crates/auths-pairing-protocol/src/lib.rs index c2057497..88061afb 100644 --- a/crates/auths-pairing-protocol/src/lib.rs +++ b/crates/auths-pairing-protocol/src/lib.rs @@ -37,8 +37,8 @@ pub mod channel_binding; pub mod domain_separation; pub mod envelope; -pub mod lookup_auth; mod error; +pub mod lookup_auth; #[cfg(feature = "pq-hybrid")] pub mod pq_hybrid; mod protocol; diff --git a/crates/auths-sdk/src/workflows/transparency.rs b/crates/auths-sdk/src/workflows/transparency.rs index 1c74516f..82e4fed0 100644 --- a/crates/auths-sdk/src/workflows/transparency.rs +++ b/crates/auths-sdk/src/workflows/transparency.rs @@ -8,9 +8,8 @@ use auths_keri::witness::independence::{ IndependencePolicy, Infrastructure, Jurisdiction, OperatorId, Organization, WitnessOperatorInfo, }; use auths_transparency::{ - BundleVerificationReport, ConsistencyProof, FsTileStore, LogOrigin, LogSigningKey, LogWriter, - MerkleHash, OfflineBundle, SignedCheckpoint, TransparencyError, TrustRoot, TrustRootWitness, - hash_leaf, + BundleVerificationReport, ConsistencyProof, FsTileStore, LogOrigin, LogWriter, MerkleHash, + OfflineBundle, SignedCheckpoint, TransparencyError, TrustRoot, TrustRootWitness, hash_leaf, }; use auths_verifier::Ed25519PublicKey; use auths_verifier::evidence_pack::TransparencyInclusion; @@ -19,9 +18,6 @@ use thiserror::Error; use crate::domains::compliance::releases::ArtifactDigest; -/// PKCS#8 signing-key file kept inside a local log directory. -const LOG_KEY_FILE: &str = "log.key"; - /// Errors from transparency verification workflows. #[derive(Debug, Error)] pub enum TransparencyWorkflowError { @@ -53,16 +49,6 @@ pub enum TransparencyWorkflowError { #[error("no transparency log at {0} — append an artifact first")] LogNotFound(PathBuf), - /// Local transparency-log file I/O failed. - #[error("transparency log I/O error at {path}: {source}")] - LogIo { - /// Path whose access failed. - path: PathBuf, - /// Underlying I/O error. - #[source] - source: std::io::Error, - }, - /// An underlying transparency-log operation (append, prove, key) failed. #[error("transparency log error: {0}")] Transparency(#[from] TransparencyError), @@ -419,41 +405,10 @@ pub struct AppendedArtifact { pub signed_checkpoint: SignedCheckpoint, } -/// Load the log signing key from `/log.key`, creating it on first -/// use when `create` is set (the append path); proving against a log that -/// does not exist yet is an error, not a key-generation event. -fn load_log_key(log_dir: &Path, create: bool) -> Result { - let path = log_dir.join(LOG_KEY_FILE); - match std::fs::read(&path) { - Ok(der) => LogSigningKey::from_pkcs8_der(&der).map_err(TransparencyWorkflowError::from), - Err(e) if e.kind() == std::io::ErrorKind::NotFound && create => { - let key = LogSigningKey::generate()?; - let der = key.to_pkcs8_der()?; - std::fs::write(&path, der).map_err(|source| TransparencyWorkflowError::LogIo { - path: path.clone(), - source, - })?; - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)).map_err( - |source| TransparencyWorkflowError::LogIo { - path: path.clone(), - source, - }, - )?; - } - Ok(key) - } - Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err( - TransparencyWorkflowError::LogNotFound(log_dir.to_path_buf()), - ), - Err(source) => Err(TransparencyWorkflowError::LogIo { path, source }), - } -} - /// Open a writer over the local tile-backed log, validating the origin and -/// (when `create`) ensuring the log directory and signing key exist. +/// (when `create`) ensuring the log directory and signing key exist. The +/// filesystem work — creating the directory and loading or minting the signing +/// key — lives on [`FsTileStore`] so the workflow stays I/O-free. fn open_log_writer( log_dir: &Path, origin: &str, @@ -461,18 +416,14 @@ fn open_log_writer( ) -> Result, TransparencyWorkflowError> { let origin = LogOrigin::new(origin) .map_err(|e| TransparencyWorkflowError::InvalidInput(format!("invalid log origin: {e}")))?; + let store = FsTileStore::new(log_dir.to_path_buf()); if create { - std::fs::create_dir_all(log_dir).map_err(|source| TransparencyWorkflowError::LogIo { - path: log_dir.to_path_buf(), - source, - })?; + store.ensure_base_dir()?; } - let key = load_log_key(log_dir, create)?; - Ok(LogWriter::new( - FsTileStore::new(log_dir.to_path_buf()), - key, - origin, - )) + let key = store + .load_or_create_key(create)? + .ok_or_else(|| TransparencyWorkflowError::LogNotFound(log_dir.to_path_buf()))?; + Ok(LogWriter::new(store, key, origin)) } /// Append an artifact digest to a local tile-backed transparency log. diff --git a/crates/auths-transparency/src/fs_store.rs b/crates/auths-transparency/src/fs_store.rs index 229c9c10..2e2ebfd1 100644 --- a/crates/auths-transparency/src/fs_store.rs +++ b/crates/auths-transparency/src/fs_store.rs @@ -2,6 +2,10 @@ use std::path::PathBuf; use crate::error::TransparencyError; use crate::store::TileStore; +use crate::writer::LogSigningKey; + +/// PKCS#8 signing-key file kept inside a local log directory. +const LOG_KEY_FILE: &str = "log.key"; /// Filesystem-backed tile store. /// @@ -35,6 +39,60 @@ impl FsTileStore { pub fn new(base_path: PathBuf) -> Self { Self { base_path } } + + /// Create the store's base directory (and any missing parents). + /// + /// Args: + /// * none beyond `self`. + /// + /// Usage: + /// ```ignore + /// store.ensure_base_dir()?; + /// ``` + pub fn ensure_base_dir(&self) -> Result<(), TransparencyError> { + std::fs::create_dir_all(&self.base_path) + .map_err(|e| TransparencyError::StoreError(e.to_string())) + } + + /// Load the log's signing key from `/log.key`, minting and + /// persisting a fresh one (mode `0600` on Unix) on first use when `create` + /// is set. + /// + /// Returns `Ok(None)` when the key is absent and `create` is false, so a + /// caller can distinguish "no log here yet" from an I/O failure without + /// itself touching the filesystem. + /// + /// Args: + /// * `create` — Generate and persist a key if none exists yet. + /// + /// Usage: + /// ```ignore + /// let key = store.load_or_create_key(true)?.expect("created on first use"); + /// ``` + pub fn load_or_create_key( + &self, + create: bool, + ) -> Result, TransparencyError> { + let path = self.base_path.join(LOG_KEY_FILE); + match std::fs::read(&path) { + Ok(der) => LogSigningKey::from_pkcs8_der(&der).map(Some), + Err(e) if e.kind() == std::io::ErrorKind::NotFound && create => { + let key = LogSigningKey::generate()?; + let der = key.to_pkcs8_der()?; + std::fs::write(&path, &der) + .map_err(|e| TransparencyError::StoreError(e.to_string()))?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)) + .map_err(|e| TransparencyError::StoreError(e.to_string()))?; + } + Ok(Some(key)) + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(e) => Err(TransparencyError::StoreError(e.to_string())), + } + } } fn is_partial_tile(path: &str) -> bool { diff --git a/crates/auths-verifier/src/action.rs b/crates/auths-verifier/src/action.rs index 700b545b..0130c042 100644 --- a/crates/auths-verifier/src/action.rs +++ b/crates/auths-verifier/src/action.rs @@ -129,7 +129,9 @@ mod tests { let parsed: ActionEnvelope = serde_json::from_str(&wire).expect("reparse envelope"); // verify_action_envelope - let canonical2 = parsed.canonical_bytes().expect("canonical bytes at verify time"); + let canonical2 = parsed + .canonical_bytes() + .expect("canonical bytes at verify time"); assert_eq!( canonical, canonical2, "canonical signing bytes changed across the JSON wire round-trip" diff --git a/deny.toml b/deny.toml index e5667f98..c06dad4a 100644 --- a/deny.toml +++ b/deny.toml @@ -77,6 +77,13 @@ ignore = [ "RUSTSEC-2023-0071", # paste unmaintained (RUSTSEC-2024-0436) — transitive via rmcp; no safe upgrade available "RUSTSEC-2024-0436", + # rmcp Streamable HTTP server DNS rebinding (RUSTSEC-2026-0189) — not exploitable + # here: rmcp is used only by auths-mcp-gateway with the `transport-io` (stdio) and + # `transport-child-process` features; the vulnerable Streamable HTTP server transport + # is never enabled, and the advisory states stdio/child-process transports are + # unaffected. The fix (rmcp >= 1.4.0) is a major upgrade from our pinned 0.9 with no + # security benefit for this transport posture. + "RUSTSEC-2026-0189", ] [sources] diff --git a/docs/security/primitive-inventory.md b/docs/security/primitive-inventory.md index 6242ad3e..28ed28cb 100644 --- a/docs/security/primitive-inventory.md +++ b/docs/security/primitive-inventory.md @@ -66,7 +66,7 @@ this section when they land. | Primitive | crate | version | |---|---|---| | X25519 ECDH (pairing) | x25519-dalek | 2 | -| ML-KEM-768 (PQ-hybrid pairing, optional, **UNAUDITED**, off by default) | ml-kem | 0.2 | +| ML-KEM-768 (PQ-hybrid pairing, optional, **UNAUDITED**, off by default) | ml-kem | 0.2.3 | ### Encoding & hygiene diff --git a/tests/scale/dashboard.html b/tests/scale/dashboard.html index 72687bc9..eda5c0ef 100644 --- a/tests/scale/dashboard.html +++ b/tests/scale/dashboard.html @@ -136,4 +136,4 @@

Batch anchoring · proposed requestAnimationFrame(() => requestAnimationFrame(() => document.querySelectorAll('.bar').forEach(b => b.style.width = b.dataset.w + '%'))); - \ No newline at end of file +