upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/grasp-audit/src/specs/grasp01/push_authorization.rs
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2025-11-27 17:05:51 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2025-11-27 17:05:51 +0000
commitd3e703d7f522c30ac6634716654c24cb7415fabd (patch)
tree0d7b64eb04c6c76a65ec06508e004f9357ca9374 /grasp-audit/src/specs/grasp01/push_authorization.rs
parentadf22539e3c1b4a96fa4b8fe04095c216b4d5541 (diff)
remove depricated code
Diffstat (limited to 'grasp-audit/src/specs/grasp01/push_authorization.rs')
-rw-r--r--grasp-audit/src/specs/grasp01/push_authorization.rs395
1 files changed, 351 insertions, 44 deletions
diff --git a/grasp-audit/src/specs/grasp01/push_authorization.rs b/grasp-audit/src/specs/grasp01/push_authorization.rs
index f1d6970..fad77fb 100644
--- a/grasp-audit/src/specs/grasp01/push_authorization.rs
+++ b/grasp-audit/src/specs/grasp01/push_authorization.rs
@@ -18,9 +18,9 @@
18 18
19use crate::{ 19use crate::{
20 clone_repo, create_commit, create_deterministic_commit, create_deterministic_commit_with_variant, 20 clone_repo, create_commit, create_deterministic_commit, create_deterministic_commit_with_variant,
21 setup_repo_with_deterministic_commit, try_push, 21 try_push, AuditClient, CommitVariant, FixtureKind, TestContext, TestResult,
22 AuditClient, CommitVariant, FixtureKind, TestContext, TestResult, DETERMINISTIC_COMMIT_HASH, 22 DETERMINISTIC_COMMIT_HASH, MAINTAINER_DETERMINISTIC_COMMIT_HASH,
23 MAINTAINER_DETERMINISTIC_COMMIT_HASH, RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH, 23 RECURSIVE_MAINTAINER_DETERMINISTIC_COMMIT_HASH,
24}; 24};
25use nostr_sdk::prelude::*; 25use nostr_sdk::prelude::*;
26use std::fs; 26use std::fs;
@@ -258,50 +258,202 @@ impl PushAuthorizationTests {
258 258
259 /// Test that push is rejected when commit doesn't match state event 259 /// Test that push is rejected when commit doesn't match state event
260 /// 260 ///
261 /// This test verifies that the relay enforces state event authorization. 261 /// GRASP-01: "MUST accept pushes via this service that match the latest repo state announcement"
262 /// The state event (from fixture) points to the deterministic commit which is 262 /// (Conversely, MUST reject pushes that don't match)
263 /// already on the server. We create a new commit locally and try to push it. 263 ///
264 /// The push should be rejected because the new commit doesn't match what the 264 /// ## Fixture-First Pattern
265 /// state event announces. 265 ///
266 /// 1. **Generate**: Create TestContext and get RepoState fixture
267 /// (repo announcement + state event pointing to deterministic commit)
268 /// 2. **Send**: Clone repo, create deterministic commit, push (establishes state on relay)
269 /// 3. **Test**: Create a NEW commit locally, try to push
270 /// 4. **Verify**: Push should be rejected because new commit doesn't match state event
266 pub async fn test_push_rejected_wrong_commit( 271 pub async fn test_push_rejected_wrong_commit(
267 client: &AuditClient, 272 client: &AuditClient,
268 git_data_dir: &Path, 273 git_data_dir: &Path,
269 relay_domain: &str, 274 relay_domain: &str,
270 ) -> TestResult { 275 ) -> TestResult {
276 use std::process::Command;
277
271 let test_name = "test_push_rejected_wrong_commit"; 278 let test_name = "test_push_rejected_wrong_commit";
272 279
273 // Set up repository with deterministic commit 280 // ============================================================
274 // This creates a state event pointing to DETERMINISTIC_COMMIT_HASH and pushes that commit 281 // Step 1: GENERATE - Create TestContext and get RepoState fixture
275 let setup = match setup_repo_with_deterministic_commit(client, git_data_dir, relay_domain).await { 282 // ============================================================
276 Ok(s) => s, 283 let ctx = TestContext::new(client);
284
285 let state_event = match ctx.get_fixture(FixtureKind::RepoState).await {
286 Ok(e) => e,
277 Err(e) => { 287 Err(e) => {
278 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") 288 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
279 .fail(&format!("Setup failed: {}", e)) 289 .fail(&format!("Failed to create RepoState fixture: {}", e));
290 }
291 };
292
293 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
294
295 // Extract repo_id and npub from state event
296 let repo_id = match state_event
297 .tags
298 .iter()
299 .find(|t| t.kind() == TagKind::d())
300 .and_then(|t| t.content())
301 {
302 Some(id) => id.to_string(),
303 None => {
304 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
305 .fail("Missing repo_id in state event");
306 }
307 };
308
309 let npub = match state_event.pubkey.to_bech32() {
310 Ok(n) => n,
311 Err(e) => {
312 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
313 .fail(&format!("Failed to convert pubkey to bech32: {}", e));
280 } 314 }
281 }; 315 };
282 316
283 // Create a new commit locally - this is NOT announced in any state event 317 // Verify repo exists on disk
284 let new_commit = match create_commit(&setup.clone_path, "Unauthorized commit") { 318 let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id));
319 if !repo_path.exists() {
320 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
321 .fail(&format!("Repo not found: {}", repo_path.display()));
322 }
323
324 // ============================================================
325 // Step 2: SEND - Clone repo, create deterministic commit, push
326 // (establishes the state on the relay)
327 // ============================================================
328 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) {
329 Ok(p) => p,
330 Err(e) => {
331 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
332 .fail(&format!("Failed to clone repo: {}", e));
333 }
334 };
335
336 // Cleanup helper
337 let cleanup = || {
338 let _ = fs::remove_dir_all(&clone_path);
339 };
340
341 // Create deterministic commit locally
342 let commit_hash = match create_deterministic_commit(&clone_path, "Initial commit") {
285 Ok(h) => h, 343 Ok(h) => h,
286 Err(e) => { 344 Err(e) => {
345 cleanup();
287 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") 346 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
288 .fail(&format!("Failed to create commit: {}", e)) 347 .fail(&format!("Failed to create deterministic commit: {}", e));
289 } 348 }
290 }; 349 };
291 350
292 // Try to push the new commit 351 // Verify commit hash matches expected
293 // This should be REJECTED because: 352 if commit_hash != DETERMINISTIC_COMMIT_HASH {
294 // - The state event still points to the deterministic commit (setup.commit_hash) 353 cleanup();
295 // - We're trying to push new_commit which is different 354 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
296 // - The relay MUST reject pushes that don't match the announced state 355 .fail(&format!(
297 let push_result = try_push(&setup.clone_path); 356 "Commit hash mismatch: got {}, expected {}",
357 commit_hash, DETERMINISTIC_COMMIT_HASH
358 ));
359 }
360
361 // Create main branch pointing to our deterministic commit
362 let branch_output = Command::new("git")
363 .args(["branch", "main"])
364 .current_dir(&clone_path)
365 .output();
366
367 match branch_output {
368 Err(e) => {
369 cleanup();
370 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
371 .fail(&format!("Failed to create main branch: {}", e));
372 }
373 Ok(output) if !output.status.success() => {
374 cleanup();
375 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
376 .fail(&format!(
377 "Failed to create main branch: {}",
378 String::from_utf8_lossy(&output.stderr)
379 ));
380 }
381 _ => {}
382 }
383
384 // Checkout main branch
385 let checkout_output = Command::new("git")
386 .args(["checkout", "main"])
387 .current_dir(&clone_path)
388 .output();
389
390 match checkout_output {
391 Err(e) => {
392 cleanup();
393 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
394 .fail(&format!("Failed to checkout main branch: {}", e));
395 }
396 Ok(output) if !output.status.success() => {
397 cleanup();
398 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
399 .fail(&format!(
400 "Failed to checkout main branch: {}",
401 String::from_utf8_lossy(&output.stderr)
402 ));
403 }
404 _ => {}
405 }
406
407 // Push the deterministic commit to establish state on relay
408 let push_output = Command::new("git")
409 .args(["push", "origin", "main"])
410 .current_dir(&clone_path)
411 .env("GIT_TERMINAL_PROMPT", "0")
412 .output();
413
414 match push_output {
415 Err(e) => {
416 cleanup();
417 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
418 .fail(&format!("Failed to push initial commit: {}", e));
419 }
420 Ok(output) if !output.status.success() => {
421 cleanup();
422 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
423 .fail(&format!(
424 "Failed to push initial commit: {}",
425 String::from_utf8_lossy(&output.stderr)
426 ));
427 }
428 _ => {}
429 }
430
431 // ============================================================
432 // Step 3: TEST - Create a NEW commit that is NOT announced
433 // in any state event
434 // ============================================================
435 let new_commit = match create_commit(&clone_path, "Unauthorized commit") {
436 Ok(h) => h,
437 Err(e) => {
438 cleanup();
439 return TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
440 .fail(&format!("Failed to create commit: {}", e));
441 }
442 };
443
444 // ============================================================
445 // Step 4: VERIFY - Push should be rejected because new commit
446 // doesn't match state event
447 // ============================================================
448 let push_result = try_push(&clone_path);
449 cleanup();
298 450
299 match push_result { 451 match push_result {
300 Ok(false) => TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event").pass(), 452 Ok(false) => TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event").pass(),
301 Ok(true) => TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event") 453 Ok(true) => TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event")
302 .fail(&format!( 454 .fail(&format!(
303 "Push accepted but should be rejected. State event points to {}, but pushed {}", 455 "Push accepted but should be rejected. State event points to {}, but pushed {}",
304 setup.commit_hash, new_commit 456 DETERMINISTIC_COMMIT_HASH, new_commit
305 )), 457 )),
306 Err(e) => TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event").fail(&e), 458 Err(e) => TestResult::new(test_name, "GRASP-01", "Push rejected when commit not in state event").fail(&e),
307 } 459 }
@@ -826,32 +978,187 @@ impl PushAuthorizationTests {
826 978
827 /// Test that non-maintainer state event is ignored 979 /// Test that non-maintainer state event is ignored
828 /// 980 ///
829 /// This test verifies that the relay ignores state events from non-maintainers. 981 /// GRASP-01: "respecting the recursive maintainer set"
830 /// We set up a valid repo, then create a rogue state event signed by a different 982 /// (Conversely, state events from non-maintainers MUST be ignored)
831 /// keypair (not the repo maintainer) that announces a different commit. The push 983 ///
832 /// should be rejected because the rogue state event is not authorized. 984 /// ## Fixture-First Pattern
985 ///
986 /// 1. **Generate**: Create TestContext and get RepoState fixture
987 /// (repo announcement + state event pointing to deterministic commit)
988 /// 2. **Send**: Clone repo, create deterministic commit, push (establishes state on relay)
989 /// 3. **Attack**: Create a rogue state event signed by a non-maintainer
990 /// 4. **Test**: Create a new commit and try to push
991 /// 5. **Verify**: Push should be rejected because rogue state event is ignored
833 pub async fn test_non_maintainer_state_rejected( 992 pub async fn test_non_maintainer_state_rejected(
834 client: &AuditClient, 993 client: &AuditClient,
835 git_data_dir: &Path, 994 git_data_dir: &Path,
836 relay_domain: &str, 995 relay_domain: &str,
837 ) -> TestResult { 996 ) -> TestResult {
997 use std::process::Command;
998
838 let test_name = "test_non_maintainer_state_rejected"; 999 let test_name = "test_non_maintainer_state_rejected";
839 1000
840 // Set up repository with deterministic commit (signed by maintainer) 1001 // ============================================================
841 let setup = match setup_repo_with_deterministic_commit(client, git_data_dir, relay_domain).await { 1002 // Step 1: GENERATE - Create TestContext and get RepoState fixture
842 Ok(s) => s, 1003 // ============================================================
1004 let ctx = TestContext::new(client);
1005
1006 let state_event = match ctx.get_fixture(FixtureKind::RepoState).await {
1007 Ok(e) => e,
1008 Err(e) => {
1009 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
1010 .fail(&format!("Failed to create RepoState fixture: {}", e));
1011 }
1012 };
1013
1014 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
1015
1016 // Extract repo_id and npub from state event
1017 let repo_id = match state_event
1018 .tags
1019 .iter()
1020 .find(|t| t.kind() == TagKind::d())
1021 .and_then(|t| t.content())
1022 {
1023 Some(id) => id.to_string(),
1024 None => {
1025 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
1026 .fail("Missing repo_id in state event");
1027 }
1028 };
1029
1030 let npub = match state_event.pubkey.to_bech32() {
1031 Ok(n) => n,
1032 Err(e) => {
1033 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
1034 .fail(&format!("Failed to convert pubkey to bech32: {}", e));
1035 }
1036 };
1037
1038 // Verify repo exists on disk
1039 let repo_path = git_data_dir.join(&npub).join(format!("{}.git", repo_id));
1040 if !repo_path.exists() {
1041 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
1042 .fail(&format!("Repo not found: {}", repo_path.display()));
1043 }
1044
1045 // ============================================================
1046 // Step 2: SEND - Clone repo, create deterministic commit, push
1047 // (establishes the state on the relay)
1048 // ============================================================
1049 let clone_path = match clone_repo(relay_domain, &npub, &repo_id) {
1050 Ok(p) => p,
1051 Err(e) => {
1052 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
1053 .fail(&format!("Failed to clone repo: {}", e));
1054 }
1055 };
1056
1057 // Cleanup helper
1058 let cleanup = || {
1059 let _ = fs::remove_dir_all(&clone_path);
1060 };
1061
1062 // Create deterministic commit locally
1063 let commit_hash = match create_deterministic_commit(&clone_path, "Initial commit") {
1064 Ok(h) => h,
843 Err(e) => { 1065 Err(e) => {
1066 cleanup();
844 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") 1067 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
845 .fail(&format!("Setup failed: {}", e)) 1068 .fail(&format!("Failed to create deterministic commit: {}", e));
846 } 1069 }
847 }; 1070 };
848 1071
849 // Create a new commit locally that we want to push 1072 // Verify commit hash matches expected
850 let new_commit = match create_commit(&setup.clone_path, "New commit to push") { 1073 if commit_hash != DETERMINISTIC_COMMIT_HASH {
1074 cleanup();
1075 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
1076 .fail(&format!(
1077 "Commit hash mismatch: got {}, expected {}",
1078 commit_hash, DETERMINISTIC_COMMIT_HASH
1079 ));
1080 }
1081
1082 // Create main branch pointing to our deterministic commit
1083 let branch_output = Command::new("git")
1084 .args(["branch", "main"])
1085 .current_dir(&clone_path)
1086 .output();
1087
1088 match branch_output {
1089 Err(e) => {
1090 cleanup();
1091 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
1092 .fail(&format!("Failed to create main branch: {}", e));
1093 }
1094 Ok(output) if !output.status.success() => {
1095 cleanup();
1096 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
1097 .fail(&format!(
1098 "Failed to create main branch: {}",
1099 String::from_utf8_lossy(&output.stderr)
1100 ));
1101 }
1102 _ => {}
1103 }
1104
1105 // Checkout main branch
1106 let checkout_output = Command::new("git")
1107 .args(["checkout", "main"])
1108 .current_dir(&clone_path)
1109 .output();
1110
1111 match checkout_output {
1112 Err(e) => {
1113 cleanup();
1114 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
1115 .fail(&format!("Failed to checkout main branch: {}", e));
1116 }
1117 Ok(output) if !output.status.success() => {
1118 cleanup();
1119 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
1120 .fail(&format!(
1121 "Failed to checkout main branch: {}",
1122 String::from_utf8_lossy(&output.stderr)
1123 ));
1124 }
1125 _ => {}
1126 }
1127
1128 // Push the deterministic commit to establish state on relay
1129 let push_output = Command::new("git")
1130 .args(["push", "origin", "main"])
1131 .current_dir(&clone_path)
1132 .env("GIT_TERMINAL_PROMPT", "0")
1133 .output();
1134
1135 match push_output {
1136 Err(e) => {
1137 cleanup();
1138 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
1139 .fail(&format!("Failed to push initial commit: {}", e));
1140 }
1141 Ok(output) if !output.status.success() => {
1142 cleanup();
1143 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
1144 .fail(&format!(
1145 "Failed to push initial commit: {}",
1146 String::from_utf8_lossy(&output.stderr)
1147 ));
1148 }
1149 _ => {}
1150 }
1151
1152 // ============================================================
1153 // Step 3: ATTACK - Create a new commit and a rogue state event
1154 // from a non-maintainer
1155 // ============================================================
1156 let new_commit = match create_commit(&clone_path, "New commit to push") {
851 Ok(h) => h, 1157 Ok(h) => h,
852 Err(e) => { 1158 Err(e) => {
1159 cleanup();
853 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") 1160 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
854 .fail(&format!("Failed to create commit: {}", e)) 1161 .fail(&format!("Failed to create commit: {}", e));
855 } 1162 }
856 }; 1163 };
857 1164
@@ -862,7 +1169,7 @@ impl PushAuthorizationTests {
862 // This event has the correct repo_id but is signed by a non-maintainer 1169 // This event has the correct repo_id but is signed by a non-maintainer
863 let rogue_state = match client 1170 let rogue_state = match client
864 .event_builder(Kind::Custom(30618), "") 1171 .event_builder(Kind::Custom(30618), "")
865 .tag(Tag::identifier(&setup.repo_id)) 1172 .tag(Tag::identifier(&repo_id))
866 .tag(Tag::custom( 1173 .tag(Tag::custom(
867 TagKind::custom("refs/heads/main"), 1174 TagKind::custom("refs/heads/main"),
868 vec![new_commit.clone()], 1175 vec![new_commit.clone()],
@@ -871,13 +1178,15 @@ impl PushAuthorizationTests {
871 { 1178 {
872 Ok(e) => e, 1179 Ok(e) => e,
873 Err(e) => { 1180 Err(e) => {
1181 cleanup();
874 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") 1182 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
875 .fail(&format!("Failed to build rogue state event: {}", e)) 1183 .fail(&format!("Failed to build rogue state event: {}", e));
876 } 1184 }
877 }; 1185 };
878 1186
879 // Send the rogue state event using the raw client to bypass AuditClient's key check 1187 // Send the rogue state event using the raw client to bypass AuditClient's key check
880 if let Err(e) = client.client().send_event(&rogue_state).await { 1188 if let Err(e) = client.client().send_event(&rogue_state).await {
1189 cleanup();
881 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored") 1190 return TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored")
882 .fail(&format!("Failed to send rogue state event: {}", e)); 1191 .fail(&format!("Failed to send rogue state event: {}", e));
883 } 1192 }
@@ -885,14 +1194,12 @@ impl PushAuthorizationTests {
885 // Wait for event to propagate 1194 // Wait for event to propagate
886 tokio::time::sleep(std::time::Duration::from_millis(200)).await; 1195 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
887 1196
888 // Try to push the new commit 1197 // ============================================================
889 // This should be REJECTED because: 1198 // Step 4 & 5: VERIFY - Push should be rejected because rogue
890 // - The rogue state event announces new_commit 1199 // state event is ignored
891 // - But the rogue state event is NOT signed by the maintainer 1200 // ============================================================
892 // - The relay should ignore the rogue state event 1201 let push_result = try_push(&clone_path);
893 // - The valid state event (from setup) still points to the deterministic commit 1202 cleanup();
894 // - Therefore pushing new_commit should fail
895 let push_result = try_push(&setup.clone_path);
896 1203
897 match push_result { 1204 match push_result {
898 Ok(false) => TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored").pass(), 1205 Ok(false) => TestResult::new(test_name, "GRASP-01", "Non-maintainer state events ignored").pass(),