From 3f74ababf338d65ac5e29e7eb5541ce416b7fe75 Mon Sep 17 00:00:00 2001 From: DanConwayDev Date: Fri, 28 Nov 2025 03:38:21 +0000 Subject: add git http advertisment allow-reachable-sha1-in-want and allow-tip-sha1-in-want --- grasp-audit/src/bin/grasp-audit.rs | 54 ++++++---- grasp-audit/src/specs/grasp01/git_clone.rs | 154 +++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+), 20 deletions(-) (limited to 'grasp-audit/src') diff --git a/grasp-audit/src/bin/grasp-audit.rs b/grasp-audit/src/bin/grasp-audit.rs index 760ed55..c0a9273 100644 --- a/grasp-audit/src/bin/grasp-audit.rs +++ b/grasp-audit/src/bin/grasp-audit.rs @@ -48,6 +48,22 @@ async fn main() -> Result<()> { match cli.command { Commands::Audit { relay, mode, spec, git_data_dir } => { + // Early validation: check if --git-data-dir is required for the spec + let specs_requiring_git_dir = ["all", "git-clone", "push-auth", "repo-creation"]; + if specs_requiring_git_dir.contains(&spec.as_str()) && git_data_dir.is_none() { + return Err(anyhow!( + "The '{}' spec requires --git-data-dir to be specified.\n\ + \n\ + This directory should point to the relay's git data storage.\n\ + Example: --git-data-dir /path/to/relay/repos\n\ + \n\ + If using Docker, mount a volume and pass that path:\n\ + docker run -v /tmp/repos:/srv/ngit-relay/repos ...\n\ + cargo run -- audit --relay ws://localhost:8080 --git-data-dir /tmp/repos", + spec + )); + } + let mut config = match mode.as_str() { "ci" => AuditConfig::ci(), "production" => AuditConfig::production(), @@ -87,7 +103,7 @@ async fn main() -> Result<()> { println!("✓ Connected\n"); - // Helper to check if git_data_dir is required + // Helper to check if git_data_dir is required for individual specs let require_git_data_dir = |spec_name: &str| -> Result { git_data_dir.clone().ok_or_else(|| { anyhow!( @@ -130,6 +146,9 @@ async fn main() -> Result<()> { specs::RepositoryCreationTests::run_all(&client, &dir).await } "all" => { + // git_data_dir is guaranteed by early validation + let dir = git_data_dir.clone().expect("git_data_dir validated earlier"); + println!("Running all tests...\n"); let mut all_results = AuditResult::new("All GRASP-01 Tests"); @@ -153,25 +172,20 @@ async fn main() -> Result<()> { let cors_results = specs::CorsTests::run_all(&client, &relay_domain).await; all_results.merge(cors_results); - // Tests that require git_data_dir - if let Some(ref dir) = git_data_dir { - // Git clone tests - println!(" → Git clone tests..."); - let clone_results = specs::GitCloneTests::run_all(&client, dir, &relay_domain).await; - all_results.merge(clone_results); - - // Push authorization tests - println!(" → Push authorization tests..."); - let push_results = specs::PushAuthorizationTests::run_all(&client, dir, &relay_domain).await; - all_results.merge(push_results); - - // Repository creation tests - println!(" → Repository creation tests..."); - let repo_results = specs::RepositoryCreationTests::run_all(&client, dir).await; - all_results.merge(repo_results); - } else { - println!(" ⚠ Skipping git-clone, push-auth, repo-creation tests (no --git-data-dir)"); - } + // Git clone tests + println!(" → Git clone tests..."); + let clone_results = specs::GitCloneTests::run_all(&client, &dir, &relay_domain).await; + all_results.merge(clone_results); + + // Push authorization tests + println!(" → Push authorization tests..."); + let push_results = specs::PushAuthorizationTests::run_all(&client, &dir, &relay_domain).await; + all_results.merge(push_results); + + // Repository creation tests + println!(" → Repository creation tests..."); + let repo_results = specs::RepositoryCreationTests::run_all(&client, &dir).await; + all_results.merge(repo_results); println!(); all_results diff --git a/grasp-audit/src/specs/grasp01/git_clone.rs b/grasp-audit/src/specs/grasp01/git_clone.rs index 8c91c04..4666a40 100644 --- a/grasp-audit/src/specs/grasp01/git_clone.rs +++ b/grasp-audit/src/specs/grasp01/git_clone.rs @@ -7,6 +7,7 @@ //! - Basic clone operation via HTTP //! - Cloned repository structure validation //! - Clone URL format verification +//! - SHA1 capability advertisement verification //! //! ## Running Tests //! @@ -34,6 +35,7 @@ impl GitCloneTests { results.add(Self::test_basic_git_clone(client, git_data_dir, relay_domain).await); results.add(Self::test_clone_url_format(client, git_data_dir, relay_domain).await); + results.add(Self::test_sha1_capabilities_advertised(client, git_data_dir, relay_domain).await); results } @@ -277,6 +279,158 @@ impl GitCloneTests { ) .pass() } + + /// Test that SHA1 capabilities are advertised in git-upload-pack + /// + /// GRASP-01 requires: + /// "MUST include `allow-reachable-sha1-in-want` and `allow-tip-sha1-in-want` + /// in advertisement and serve available oids." + /// + /// This test verifies: + /// 1. The info/refs endpoint returns the capabilities + /// 2. Both allow-reachable-sha1-in-want and allow-tip-sha1-in-want are present + pub async fn test_sha1_capabilities_advertised( + client: &AuditClient, + git_data_dir: &Path, + relay_domain: &str, + ) -> TestResult { + let test_name = "test_sha1_capabilities_advertised"; + let ctx = TestContext::new(client); + + // Create repository announcement + let repo = match ctx.get_fixture(FixtureKind::ValidRepo).await { + Ok(r) => r, + Err(e) => { + return TestResult::new( + test_name, + "GRASP-01", + "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", + ) + .fail(&format!("Failed to create repo fixture: {}", e)) + } + }; + + // Wait for repository creation + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + + // Extract repo identifier and npub + let repo_id = match repo + .tags + .iter() + .find(|t| t.kind() == TagKind::d()) + .and_then(|t| t.content()) + { + Some(id) => id.to_string(), + None => { + return TestResult::new( + test_name, + "GRASP-01", + "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", + ) + .fail("Repository announcement missing d tag") + } + }; + + let npub = match repo.pubkey.to_bech32() { + Ok(n) => n, + Err(e) => { + return TestResult::new( + test_name, + "GRASP-01", + "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", + ) + .fail(&format!("Failed to convert pubkey to npub: {}", e)) + } + }; + + // Verify repository exists + let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id)); + if !repo_path.exists() { + return TestResult::new( + test_name, + "GRASP-01", + "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", + ) + .fail(&format!( + "Repository not found at: {}", + repo_path.display() + )); + } + + // Build info/refs URL for git-upload-pack service + let info_refs_url = format!( + "http://{}/{}/{}.git/info/refs?service=git-upload-pack", + relay_domain, npub, repo_id + ); + + // Make HTTP request to get the advertisement + let http_client = reqwest::Client::new(); + let response = match http_client.get(&info_refs_url).send().await { + Ok(r) => r, + Err(e) => { + return TestResult::new( + test_name, + "GRASP-01", + "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", + ) + .fail(&format!("HTTP request failed: {}", e)) + } + }; + + if !response.status().is_success() { + return TestResult::new( + test_name, + "GRASP-01", + "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", + ) + .fail(&format!( + "info/refs request failed with status: {}", + response.status() + )); + } + + // Get response body + let body = match response.text().await { + Ok(b) => b, + Err(e) => { + return TestResult::new( + test_name, + "GRASP-01", + "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", + ) + .fail(&format!("Failed to read response body: {}", e)) + } + }; + + // Check for required capabilities + let has_allow_reachable = body.contains("allow-reachable-sha1-in-want"); + let has_allow_tip = body.contains("allow-tip-sha1-in-want"); + + if !has_allow_reachable { + return TestResult::new( + test_name, + "GRASP-01", + "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", + ) + .fail("Missing capability: allow-reachable-sha1-in-want"); + } + + if !has_allow_tip { + return TestResult::new( + test_name, + "GRASP-01", + "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", + ) + .fail("Missing capability: allow-tip-sha1-in-want"); + } + + TestResult::new( + test_name, + "GRASP-01", + "MUST include allow-reachable-sha1-in-want and allow-tip-sha1-in-want in advertisement", + ) + .pass() + } } #[cfg(test)] -- cgit v1.2.3