1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
use super::helpers::TestEnv;
/// Tests key rotation and its effect on commit verification.
///
/// NOTE: `emergency rotate-now` currently uses the legacy GitKel storage backend,
/// but `init` creates identities using the packed registry backend (`refs/auths/registry`).
/// This storage mismatch means rotation fails with "KEL not found for prefix".
/// When this is fixed, remove the early return and test the full rotation flow.
#[test]
fn test_key_rotation_supersedes_old_commit_verification() {
let env = TestEnv::new();
env.init_identity();
// Commit A: signed with original key
std::fs::write(env.repo_path.join("a.txt"), "commit A").unwrap();
let add = env.git_cmd().args(["add", "a.txt"]).output().unwrap();
assert!(add.status.success());
let commit = env
.git_cmd()
.args(["commit", "-m", "commit A"])
.output()
.unwrap();
assert!(
commit.status.success(),
"commit A failed: {}",
String::from_utf8_lossy(&commit.stderr)
);
// Add the in-band Auths-Id / Auths-Device trailers. This amends commit A, so the
// verifiable hash is captured *after* signing.
let sign = env.cmd("auths").args(["sign", "HEAD"]).output().unwrap();
assert!(
sign.status.success(),
"auths sign failed: {}",
String::from_utf8_lossy(&sign.stderr)
);
let log_a = env.git_cmd().args(["rev-parse", "HEAD"]).output().unwrap();
let commit_a_hash = String::from_utf8_lossy(&log_a.stdout).trim().to_string();
// Verify commit A works (KEL-native — no allowlist).
let verify_a = env
.cmd("auths")
.args(["verify", &commit_a_hash])
.output()
.unwrap();
assert!(
verify_a.status.success(),
"commit A should verify, stderr: {}",
String::from_utf8_lossy(&verify_a.stderr)
);
// Attempt rotation — may fail due to GitKel/registry storage mismatch
let rotate = env
.cmd("auths")
.args([
"emergency",
"rotate-now",
"--yes",
"--current-alias",
"main",
"--next-alias",
"main-rotated",
])
.output()
.unwrap();
if !rotate.status.success() {
let stderr = String::from_utf8_lossy(&rotate.stderr);
// Known issues:
// - rotate-now uses GitKel backend but init uses registry storage
// - P-256 keys can't be rotated yet (rotation code assumes Ed25519)
if stderr.contains("KEL not found")
|| stderr.contains("Unrecognized Ed25519")
|| stderr.contains("key decryption failed")
{
eprintln!(
"Skipping post-rotation assertions: \
rotation not yet supported for current key type/backend"
);
return;
}
panic!("rotation failed unexpectedly: {}", stderr);
}
// If rotation succeeded: under current-key KEL verification, commit A was signed by
// the now-superseded key, so it no longer verifies as current. Preserving old-key
// commits across a rotation requires signing-time (anchored) verification — tracked
// as #205.
let verify_a_after = env
.cmd("auths")
.args(["verify", &commit_a_hash])
.output()
.unwrap();
assert!(
!verify_a_after.status.success(),
"after rotation, an old-key commit is superseded under current-key verify (#205)"
);
}