crates/gunnar-cli/tests/http_push_is_attributable.rs
#![cfg(feature = "http")]
//! **GUN-014: an audit record can name the credential that pushed, on both
//! transports.**
//!
//! `session_key_fingerprint` returns an empty string when
//! `ServiceRequest.key_fingerprint` is `None`, and its own doc stated that an
//! empty value has *"one reason only — the anonymous, read-only path… That path
//! cannot push, so it cannot reach an attestation."*
//!
//! Smart HTTP passes `None` for **every** request, `git-receive-pack`
//! included: HTTP authenticates with a token and there is no key to name. So
//! the claim was false exactly where it mattered — `RefUpdated`, `RefDeleted`
//! and the push attestation all carried an empty fingerprint for an
//! authenticated write, and two HTTP pushes by two different people produced
//! the same audit line.
//!
//! The fix is a `credential` field, not a fingerprint that lies: `key:SHA256:…`
//! over SSH, `token:<hash prefix>` over HTTP, empty only for a caller that
//! presented nothing. This file pushes over BOTH transports into one server and
//! reads the attestation log back, because the property is that the two
//! transports answer the same question in the same vocabulary — one of them
//! alone would not show that.
mod common;
use std::path::{Path, PathBuf};
use common::{tools_present, Harness, HarnessOptions};
use gunnar_core::{HashKind, Visibility};
const NS: &str = "team";
const REPO: &str = "project";
/// A local repository with one commit on `main`.
fn local(harness: &Harness, at: &Path, text: &str) {
harness
.git(
harness.dir.path(),
&["init", "--quiet", "-b", "main", at.to_str().expect("utf-8")],
)
.expect_ok("git init");
std::fs::write(at.join("a.txt"), text).expect("write");
harness.git(at, &["add", "."]).expect_ok("add");
harness
.git(at, &["commit", "--quiet", "-m", text])
.expect_ok("commit");
}
/// The `credential` field of the most recent attestation.
fn last_credential(harness: &Harness) -> (String, String) {
let records = harness
.attestation_log()
.read()
.expect("read attestations.jsonl");
let last = records.last().expect("a push writes an attestation");
(last.credential.clone(), last.key_fingerprint.clone())
}
/// **The finding, and its other half.**
///
/// Mutation-proven 2026-09-11: drop the `with_credential` call from
/// `gunnar-http`'s service and the HTTP push's `credential` is the empty
/// string — indistinguishable from the anonymous read the field's own doc says
/// is the only thing an empty value can mean.
#[test]
fn a_push_names_its_credential_over_ssh_and_over_http() {
assert!(tools_present(), "this test requires git and ssh");
let mut harness = Harness::start_with_options(
NS,
REPO,
HashKind::Sha1,
HarnessOptions {
http: true,
visibility: Visibility::Private,
..HarnessOptions::default()
},
);
// ── over SSH: the key ──────────────────────────────────────────────────
let ssh_work: PathBuf = harness.dir.path().join("over-ssh");
local(&harness, &ssh_work, "over ssh\n");
harness
.git(
&ssh_work,
&["push", "--quiet", &harness.url(NS, REPO), "main"],
)
.expect_ok("push over ssh");
let (credential, fingerprint) = last_credential(&harness);
assert_eq!(
credential,
format!("key:{}", harness.key_fingerprint),
"an SSH push must be attributed to the key that authenticated it"
);
assert_eq!(
fingerprint, harness.key_fingerprint,
"and the fingerprint field is unchanged, so nothing that reads it broke"
);
// ── over HTTP: the token ───────────────────────────────────────────────
//
// The same server, the same principal, a different credential. Before the
// fix this push produced an attestation with an empty fingerprint and
// nothing else naming who made it.
let token = harness.mint_token("tester");
let url = harness.http_url_with_token(NS, REPO, &token);
let http_work = harness.dir.path().join("over-http");
local(&harness, &http_work, "over http\n");
harness
.git_http(&http_work, &["push", "--quiet", "--force", &url, "main"])
.expect_ok("push over http");
let (credential, fingerprint) = last_credential(&harness);
assert!(
credential.starts_with("token:"),
"an HTTP push must be attributed to the token that authenticated it; the record said \
{credential:?}"
);
assert!(
!credential.ends_with(':'),
"the credential must carry a hash prefix, not a bare scheme: {credential:?}"
);
assert!(
!credential.contains(&token),
"the audit record must never carry the token itself: {credential:?}"
);
assert!(
credential.len() < "token:".len() + 32,
"a PREFIX of the hash, not the whole of it: {credential:?}"
);
assert!(
fingerprint.is_empty(),
"there is no key on the HTTP path, and inventing one would be worse than an empty \
field: {fingerprint:?}"
);
// …and the two pushes are distinguishable, which is the whole of what the
// audit trail could not do. Two records, two different credentials.
let records = harness
.attestation_log()
.read()
.expect("read attestations.jsonl");
assert_eq!(records.len(), 2, "two pushes, two attestations");
assert_ne!(
records[0].credential, records[1].credential,
"two pushes by two different credentials must not produce the same audit line"
);
harness.stop();
}