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
46 changes: 42 additions & 4 deletions crates/auths-cli/src/commands/artifact/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -501,9 +501,30 @@ pub async fn handle_verify(file: &Path, args: VerifyArtifactArgs) -> Result<()>
valid = false;
}
Some(sha) => {
// Verify the commit is signed by a trusted key.
// Uses in-process verification via auths-verifier (no git shell-out).
let commit_sig_ok = verify_commit_in_process(sha).await;
// Verify the commit is signed by a trusted key. With an identity
// bundle the commit leg verifies against the bundle's evidenced
// KELs (the stateless CI posture — same doctrine as
// `auths verify <sha> --identity-bundle`); otherwise the local
// registry resolves the signer in-process (no git shell-out).
let commit_sig_ok = match &identity_bundle {
Some(bundle_path) => {
match crate::commands::verify_commit::commit_trusted_via_bundle(
sha,
bundle_path,
)
.await
{
Ok(()) => true,
Err(reason) => {
if !is_json_mode() {
eprintln!(" Commit-anchor leg failed: {reason}");
}
false
}
}
}
None => verify_commit_in_process(sha).await,
};

if !commit_sig_ok {
valid = false;
Expand Down Expand Up @@ -643,7 +664,12 @@ pub async fn handle_verify(file: &Path, args: VerifyArtifactArgs) -> Result<()>
commit_sha: commit_sha_val,
commit_verified,
oidc_join,
error: log_anchor_error.or(oidc_error),
error: log_anchor_error.or(oidc_error).or_else(|| {
(!valid).then(|| {
"attestation chain or its commit-anchor leg did not verify (see lines above)"
.to_string()
})
}),
},
)
}
Expand Down Expand Up @@ -703,6 +729,18 @@ fn resolve_identity_key(
identity_bundle: &Option<PathBuf>,
attestation: &Attestation,
) -> Result<(auths_verifier::DevicePublicKey, CanonicalDid)> {
// An ephemeral (`did:key:`) attestation is SELF-signed: its chain leg can only
// verify against the issuer's own in-band key — the bundle's root never signed
// it. The bundle still anchors the COMMIT leg (artifact ← ephemeral key ←
// commit ← maintainer), enforced in the ephemeral commit-anchor step below.
if attestation.issuer.as_str().starts_with("did:key:") {
let issuer = &attestation.issuer;
let (pk_bytes, curve) = resolve_pk_from_did(issuer)
.with_context(|| format!("Failed to resolve public key from issuer DID '{issuer}'"))?;
let pk = auths_verifier::DevicePublicKey::try_new(curve, &pk_bytes)
.map_err(|e| anyhow!("Invalid issuer public key resolved from DID: {e}"))?;
return Ok((pk, issuer.clone()));
}
if let Some(bundle_path) = identity_bundle {
let bundle_content = fs::read_to_string(bundle_path)
.with_context(|| format!("Failed to read identity bundle: {:?}", bundle_path))?;
Expand Down
92 changes: 92 additions & 0 deletions crates/auths-cli/src/commands/verify_commit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,98 @@ fn raw_commit_object(sha: &str) -> Result<String> {

/// Map a [`CommitVerdict`] onto a CLI result row: the valid flag, the verified signer,
/// and a human-readable reason for every failure mode.
/// Bundle-evidence commit trust for sibling commands — the artifact verifier's
/// ephemeral commit-anchor leg. Same doctrine as `auths verify <sha>
/// --identity-bundle`: the repo's pinned roots are the anchor, the bundle is KEL
/// EVIDENCE only (RT-005), and the verdict tolerates the bundle's offline
/// freshness grade exactly as the commit command does.
///
/// Args:
/// * `sha`: the full commit SHA the attestation anchors to.
/// * `bundle_path`: the identity bundle supplied to the artifact verify.
///
/// Usage:
/// ```ignore
/// commit_trusted_via_bundle(&sha, Path::new(".auths/ci-bundle.json")).await?;
/// ```
pub(crate) async fn commit_trusted_via_bundle(
sha: &str,
bundle_path: &std::path::Path,
) -> std::result::Result<(), String> {
let raw_commit = raw_commit_object(sha).map_err(|e| e.to_string())?;
let (trailer_root_did, device_did) =
auths_sdk::workflows::commit_trust::commit_signer_trailers(&raw_commit)
.ok_or_else(|| "commit carries no Auths-Id/Auths-Device trailers".to_string())?;

let pinned_roots = super::verify_helpers::load_project_pinned_roots();
if pinned_roots.is_empty() {
return Err(
"no pinned roots (.auths/roots) — a bundle is evidence for a pinned root, \
never the source of the pin"
.to_string(),
);
}
#[allow(clippy::disallowed_methods)] // presentation boundary: freshness needs the wall clock
let now = chrono::Utc::now();
let (root, kel, device_kels) = load_bundle_trust(bundle_path, now)?;
if !pinned_roots.contains(&root) {
return Err(format!(
"identity bundle root {root} is not independently trusted: add it to .auths/roots"
));
}
let mut bundle_kels: Vec<BundleKel> = Vec::new();
if !kel.is_empty() {
bundle_kels.push(BundleKel {
did: root,
events: kel,
});
}
bundle_kels.extend(
device_kels
.into_iter()
.map(|(did, events)| BundleKel { did, events }),
);

let auths_home = auths_sdk::paths::auths_home().map_err(|e| e.to_string())?;
let registry =
GitRegistryBackend::from_config_unchecked(RegistryConfig::single_tenant(&auths_home));
let device_kel = resolve_signer_kel(&registry, &bundle_kels, &device_did)
.await
.map_err(|e| format!("device KEL for {device_did} could not be resolved: {e}"))?;
let root_did = match device_kel.first().and_then(|e| e.delegator()) {
Some(delegator) => format!("did:keri:{delegator}"),
None => trailer_root_did,
};
let root_kel = resolve_signer_kel(&registry, &bundle_kels, &root_did)
.await
.map_err(|e| format!("root KEL for {root_did} could not be resolved: {e}"))?;

let provider = auths_crypto::RingCryptoProvider;
let receipt_lookup = GitWitnessReceiptLookup::new(&auths_home);
let witnessed = verify_commit_against_kel_witnessed_scoped(
raw_commit.as_bytes(),
&device_kel,
&root_kel,
&pinned_roots,
&provider,
&receipt_lookup,
VerifierWitnessPolicy::Warn,
now.timestamp(),
)
.await;
let verdict = witnessed
.verdict
.with_freshness(&FreshnessPolicy::default(), FreshnessEvidence::Offline);
let result = verdict_to_result(sha.to_string(), verdict);
if result.valid {
Ok(())
} else {
Err(result.error.unwrap_or_else(|| {
"commit did not verify against the bundle-evidenced KELs".to_string()
}))
}
}

fn verdict_to_result(commit: String, verdict: CommitVerdict) -> VerifyCommitResult {
let mut result = VerifyCommitResult::failure(commit, String::new());
// The stable machine code travels in the `status` field regardless of the
Expand Down
Loading