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
2 changes: 2 additions & 0 deletions crates/auths-cli/src/commands/artifact/batch_sign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pub fn handle_batch_sign(
attestation_dir: Option<PathBuf>,
expires_in: Option<u64>,
note: Option<String>,
commit_sha: Option<String>,
repo_opt: Option<PathBuf>,
passphrase_provider: Arc<dyn PassphraseProvider + Send + Sync>,
env_config: &EnvironmentConfig,
Expand Down Expand Up @@ -67,6 +68,7 @@ pub fn handle_batch_sign(
identity_key: key.map(|s| s.to_string()),
expires_in,
note,
commit_sha,
};

let result = batch_sign_artifacts(config, &ctx)
Expand Down
58 changes: 58 additions & 0 deletions crates/auths-cli/src/commands/artifact/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use std::sync::Arc;
use anyhow::{Result, bail};
use auths_core::config::EnvironmentConfig;
use auths_core::signing::PassphraseProvider;
use auths_sdk::signing::validate_commit_sha;

#[derive(Args, Debug, Clone)]
#[command(
Expand Down Expand Up @@ -73,6 +74,14 @@ pub enum ArtifactSubcommand {
/// Optional note to embed in the attestation.
#[arg(long)]
note: Option<String>,

/// Git commit SHA to embed in the attestation (auto-detected from HEAD if omitted).
#[arg(long, conflicts_with = "no_commit")]
commit: Option<String>,

/// Do not embed any commit SHA in the attestation.
#[arg(long, conflicts_with = "commit")]
no_commit: bool,
},

/// Sign and publish an artifact attestation to a registry.
Expand Down Expand Up @@ -110,6 +119,14 @@ pub enum ArtifactSubcommand {
/// Optional note to embed in the attestation.
#[arg(long)]
note: Option<String>,

/// Git commit SHA to embed in the attestation (auto-detected from HEAD if omitted).
#[arg(long, conflicts_with = "no_commit")]
commit: Option<String>,

/// Do not embed any commit SHA in the attestation.
#[arg(long, conflicts_with = "commit")]
no_commit: bool,
},

/// Sign multiple artifacts matching a glob pattern.
Expand Down Expand Up @@ -140,6 +157,14 @@ pub enum ArtifactSubcommand {
/// Optional note to embed in each attestation.
#[arg(long)]
note: Option<String>,

/// Git commit SHA to embed in the attestation (auto-detected from HEAD if omitted).
#[arg(long, conflicts_with = "no_commit")]
commit: Option<String>,

/// Do not embed any commit SHA in the attestation.
#[arg(long, conflicts_with = "commit")]
no_commit: bool,
},

/// Verify an artifact's signature against an Auths identity.
Expand Down Expand Up @@ -167,9 +192,28 @@ pub enum ArtifactSubcommand {
/// Witness quorum threshold (default: 1).
#[arg(long, default_value = "1")]
witness_threshold: usize,

/// Also verify the source commit's signing attestation.
#[arg(long)]
verify_commit: bool,
},
}

/// Resolve the commit SHA from CLI flags.
fn resolve_commit_sha_from_flags(
commit: Option<String>,
no_commit: bool,
) -> Result<Option<String>> {
if no_commit {
return Ok(None);
}
if let Some(sha) = commit {
let validated = validate_commit_sha(&sha).map_err(|e| anyhow::anyhow!("{}", e))?;
return Ok(Some(validated));
}
Ok(crate::commands::git_helpers::resolve_head_silent())
}

/// Handle the `artifact` command dispatch.
pub fn handle_artifact(
cmd: ArtifactCommand,
Expand All @@ -185,7 +229,10 @@ pub fn handle_artifact(
device_key,
expires_in,
note,
commit,
no_commit,
} => {
let commit_sha = resolve_commit_sha_from_flags(commit, no_commit)?;
let resolved_alias = match device_key {
Some(alias) => alias,
None => crate::commands::key_detect::auto_detect_device_key(
Expand All @@ -200,6 +247,7 @@ pub fn handle_artifact(
&resolved_alias,
expires_in,
note,
commit_sha,
repo_opt,
passphrase_provider,
env_config,
Expand All @@ -214,7 +262,10 @@ pub fn handle_artifact(
device_key,
expires_in,
note,
commit,
no_commit,
} => {
let commit_sha = resolve_commit_sha_from_flags(commit, no_commit)?;
let sig_path = match (signature, file.as_ref()) {
(Some(sig), _) => sig,
(None, Some(artifact)) => {
Expand All @@ -236,6 +287,7 @@ pub fn handle_artifact(
&resolved_alias,
expires_in,
note,
commit_sha,
repo_opt.clone(),
passphrase_provider,
env_config,
Expand All @@ -256,7 +308,10 @@ pub fn handle_artifact(
attestation_dir,
expires_in,
note,
commit,
no_commit,
} => {
let commit_sha = resolve_commit_sha_from_flags(commit, no_commit)?;
let resolved_alias = match device_key {
Some(alias) => alias,
None => crate::commands::key_detect::auto_detect_device_key(
Expand All @@ -271,6 +326,7 @@ pub fn handle_artifact(
attestation_dir,
expires_in,
note,
commit_sha,
repo_opt,
passphrase_provider,
env_config,
Expand All @@ -283,6 +339,7 @@ pub fn handle_artifact(
witness_receipts,
witness_keys,
witness_threshold,
verify_commit,
} => {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(verify::handle_verify(
Expand All @@ -292,6 +349,7 @@ pub fn handle_artifact(
witness_receipts,
&witness_keys,
witness_threshold,
verify_commit,
))
}
}
Expand Down
2 changes: 2 additions & 0 deletions crates/auths-cli/src/commands/artifact/sign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub fn handle_sign(
device_key: &str,
expires_in: Option<u64>,
note: Option<String>,
commit_sha: Option<String>,
repo_opt: Option<PathBuf>,
passphrase_provider: Arc<dyn PassphraseProvider + Send + Sync>,
env_config: &EnvironmentConfig,
Expand All @@ -35,6 +36,7 @@ pub fn handle_sign(
device_key: SigningKeyMaterial::Alias(KeyAlias::new_unchecked(device_key)),
expires_in,
note,
commit_sha,
};

let result = sign_artifact(params, &ctx)
Expand Down
60 changes: 60 additions & 0 deletions crates/auths-cli/src/commands/artifact/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ struct VerifyArtifactResult {
#[serde(skip_serializing_if = "Option::is_none")]
issuer: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
commit_sha: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
commit_verified: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<String>,
}

Expand All @@ -50,6 +54,7 @@ pub async fn handle_verify(
witness_receipts: Option<PathBuf>,
witness_keys: &[String],
witness_threshold: usize,
verify_commit: bool,
) -> Result<()> {
let file_str = file.to_string_lossy().to_string();

Expand Down Expand Up @@ -139,6 +144,8 @@ pub async fn handle_verify(
capability_valid: None,
witness_quorum: None,
issuer: Some(attestation.issuer.to_string()),
commit_sha: attestation.commit_sha.clone(),
commit_verified: None,
error: Some(format!(
"Digest mismatch: file={}, attestation={}",
file_digest.hex, artifact_meta.digest.hex
Expand Down Expand Up @@ -201,6 +208,55 @@ pub async fn handle_verify(
valid = false;
}

// 8b. Display commit linkage info (always, when present)
let commit_sha_val = attestation.commit_sha.clone();
if let Some(ref sha) = commit_sha_val
&& !is_json_mode()
{
eprintln!(" Commit: {}", sha);
}

// 8c. Optional commit attestation verification
let commit_verified = if verify_commit {
match &commit_sha_val {
None => {
if !is_json_mode() {
eprintln!(
"warning: artifact attestation has no commit_sha field; \
re-sign with: auths artifact sign --commit <SHA>"
);
}
None
}
Some(sha) => {
// Look up commit attestation via git ref
let commit_ref = format!("refs/auths/commits/{}", sha);
let lookup = std::process::Command::new("git")
.args(["show", &format!("{}:attestation.json", commit_ref)])
.output();
match lookup {
Ok(output) if output.status.success() => {
if !is_json_mode() {
eprintln!(" Commit {}: signing attestation found", &sha[..12]);
}
Some(true)
}
_ => {
if !is_json_mode() {
eprintln!(
"warning: no signing attestation found for commit {}",
&sha[..std::cmp::min(sha.len(), 12)]
);
}
Some(false)
}
}
}
}
} else {
None
};

let exit_code = if valid { 0 } else { 1 };

output_result(
Expand All @@ -214,6 +270,8 @@ pub async fn handle_verify(
capability_valid,
witness_quorum,
issuer: Some(identity_did.to_string()),
commit_sha: commit_sha_val,
commit_verified,
error: None,
},
)
Expand Down Expand Up @@ -313,6 +371,8 @@ fn output_error(file: &str, exit_code: i32, message: &str) -> Result<()> {
capability_valid: None,
witness_quorum: None,
issuer: None,
commit_sha: None,
commit_verified: None,
error: Some(message.to_string()),
};
println!("{}", serde_json::to_string(&result)?);
Expand Down
49 changes: 49 additions & 0 deletions crates/auths-cli/src/commands/git_helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use anyhow::{Context, Result, anyhow};
use std::process::Command;

/// Resolve a git ref to a full commit SHA.
///
/// Args:
/// * `commit_ref`: A git ref (branch name, tag, SHA, HEAD, etc.).
///
/// Usage:
/// ```ignore
/// let sha = resolve_commit_sha("HEAD")?;
/// let sha = resolve_commit_sha("v1.0.0")?;
/// ```
pub fn resolve_commit_sha(commit_ref: &str) -> Result<String> {
let output = Command::new("git")
.args(["rev-parse", commit_ref])
.output()
.context("Failed to resolve commit reference")?;

if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(anyhow!(
"Invalid commit reference '{}': {}",
commit_ref,
stderr.trim()
));
}

Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}

/// Silently attempt to resolve HEAD to a full commit SHA.
///
/// Returns `None` if not in a git repo, no commits exist, or git is unavailable.
///
/// Usage:
/// ```ignore
/// let sha = resolve_head_silent(); // Some("abc123...") or None
/// ```
pub fn resolve_head_silent() -> Option<String> {
Command::new("git")
.args(["rev-parse", "--verify", "--quiet", "HEAD"])
.output()
.ok()
.filter(|o| o.status.success())
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
}
1 change: 1 addition & 0 deletions crates/auths-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub mod doctor;
pub mod emergency;
pub mod error_lookup;
pub mod git;
pub mod git_helpers;
pub mod id;
pub mod index;
pub mod init;
Expand Down
2 changes: 2 additions & 0 deletions crates/auths-cli/src/commands/org.rs
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ pub fn handle_org(
admin_capabilities,
Some(Role::Admin),
None, // Root admin has no delegator
None, // commit_sha
)
.context("Failed to create admin attestation")?;

Expand Down Expand Up @@ -499,6 +500,7 @@ pub fn handle_org(
vec![],
None,
None,
None, // commit_sha
)
.context("Failed to create signed attestation object")?;

Expand Down
2 changes: 2 additions & 0 deletions crates/auths-cli/src/commands/sign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,13 +152,15 @@ pub fn handle_sign_unified(
Some(alias) => alias.to_string(),
None => super::key_detect::auto_detect_device_key(repo_opt.as_deref(), env_config)?,
};
let commit_sha = super::git_helpers::resolve_head_silent();
handle_artifact_sign(
&path,
cmd.sig_output,
cmd.key.as_deref(),
&device_key_alias,
cmd.expires_in,
cmd.note,
commit_sha,
repo_opt,
passphrase_provider,
env_config,
Expand Down
Loading
Loading