Skip to content
Merged
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
5 changes: 1 addition & 4 deletions .github/workflows/cargo-deny.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}

Expand Down
8 changes: 4 additions & 4 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion crates/auths-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 1 addition & 3 deletions crates/auths-cli/src/commands/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
16 changes: 12 additions & 4 deletions crates/auths-cli/src/commands/id/identity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,

/// Write to this file instead of stdout.
Expand Down Expand Up @@ -911,13 +914,18 @@ pub fn handle_id(
// allowed_signers line: `<principal> <keytype> <base64>`. The OpenSSH
// encoding already carries `<keytype> <base64>`; trim any trailing
// comment so the line is exactly the two fields ssh expects.
let key_field = openssh.split_whitespace().take(2).collect::<Vec<_>>().join(" ");
let key_field = openssh
.split_whitespace()
.take(2)
.collect::<Vec<_>>()
.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);
}
Expand Down
13 changes: 4 additions & 9 deletions crates/auths-cli/src/commands/kel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,19 +112,14 @@ fn load_events(args: &KelValidateArgs, ctx: &CliConfig) -> Result<(String, Vec<E
return Ok((path.display().to_string(), events));
}

let auths_home = auths_sdk::paths::auths_home()?;
let auths_home = auths_sdk::storage_layout::resolve_repo_path(ctx.repo_path.clone())?;
let did = match &args.did {
Some(d) => 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")
})?
}
};
Expand Down
2 changes: 1 addition & 1 deletion crates/auths-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Empty file removed crates/auths-cli/src/ux/dialogs.rs
Empty file.
1 change: 0 additions & 1 deletion crates/auths-cli/src/ux/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
pub mod dialogs;
pub mod format;
pub mod ident;

Expand Down
11 changes: 6 additions & 5 deletions crates/auths-infra-http/src/pairing_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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?;
Expand Down
2 changes: 1 addition & 1 deletion crates/auths-pairing-protocol/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
71 changes: 11 additions & 60 deletions crates/auths-sdk/src/workflows/transparency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -419,60 +405,25 @@ pub struct AppendedArtifact {
pub signed_checkpoint: SignedCheckpoint,
}

/// Load the log signing key from `<log_dir>/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<LogSigningKey, TransparencyWorkflowError> {
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,
create: bool,
) -> Result<LogWriter<FsTileStore>, 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.
Expand Down
58 changes: 58 additions & 0 deletions crates/auths-transparency/src/fs_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -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 `<base>/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<Option<LogSigningKey>, 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 {
Expand Down
4 changes: 3 additions & 1 deletion crates/auths-verifier/src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 7 additions & 0 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion docs/security/primitive-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion tests/scale/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,4 @@ <h4><i class="chip s2"></i>Batch anchoring <span class="muted">&middot; proposed
requestAnimationFrame(() => requestAnimationFrame(() =>
document.querySelectorAll('.bar').forEach(b => b.style.width = b.dataset.w + '%')));
</script>
</body></html>
</body></html>
Loading