Skip to main content

lean_ctx/core/
integrity.rs

1//! Build-origin integrity verification and rebrand resistance.
2//!
3//! Detects if the binary has been modified by automated rebranding tools
4//! (e.g. `sed s/lean-ctx/better-ctx/g`). The integrity seed is a compile-time
5//! constant; its hash is precomputed and embedded. If the seed is altered by
6//! a text-replacement tool, the hash will no longer match.
7
8use std::collections::hash_map::DefaultHasher;
9use std::hash::{Hash, Hasher};
10
11const INTEGRITY_SEED: &str = "lean-ctx";
12const ORIGIN_REPO: &str = env!("CARGO_PKG_REPOSITORY");
13const ORIGIN_NAME: &str = env!("CARGO_PKG_NAME");
14const PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
15
16fn compute_seed_hash(seed: &str) -> u64 {
17    let mut hasher = DefaultHasher::new();
18    seed.hash(&mut hasher);
19    hasher.finish()
20}
21
22fn expected_hash() -> u64 {
23    compute_seed_hash("lean-ctx")
24}
25
26pub(crate) fn verify_integrity() -> bool {
27    compute_seed_hash(INTEGRITY_SEED) == expected_hash()
28}
29
30pub(crate) fn is_official_origin() -> bool {
31    ORIGIN_REPO.contains("yvgude/lean-ctx") && ORIGIN_NAME == "lean-ctx-engine"
32}
33
34/// Which pipeline built this binary, stamped by `build.rs` at compile time
35/// (never a runtime git call). `official` is the upstream release workflow
36/// only; `dogfood` the promoted Instance state, `dev` any other checkout,
37/// `source` a packaged crate or a tree without git.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub struct BuildInfo {
40    pub kind: &'static str,
41    /// Full 40-hex commit; empty when the build had none.
42    pub commit: &'static str,
43    /// Tracked files differed from `commit` when the build script last ran.
44    pub dirty: bool,
45}
46
47/// Commit digits shown in labels (`lean-ctx 3.12.2 (dogfood f78aff8dc5)`).
48const SHORT_COMMIT: usize = 10;
49
50impl BuildInfo {
51    pub fn short_commit(&self) -> &'static str {
52        self.commit.get(..SHORT_COMMIT).unwrap_or(self.commit)
53    }
54
55    pub fn is_official(&self) -> bool {
56        self.kind == "official"
57    }
58
59    /// `official`, `dogfood f78aff8dc5`, `dev 1a2b3c4d5e, dirty`, `source`.
60    pub fn label(&self) -> String {
61        if self.is_official() {
62            return "official".to_owned();
63        }
64        let mut label = self.kind.to_owned();
65        if !self.commit.is_empty() {
66            label.push(' ');
67            label.push_str(self.short_commit());
68        }
69        if self.dirty {
70            label.push_str(", dirty");
71        }
72        label
73    }
74
75    /// The additive `build` object of `--version --json` and the
76    /// capabilities documents.
77    pub fn to_json(&self) -> serde_json::Value {
78        serde_json::json!({
79            "kind": self.kind,
80            "commit": (!self.commit.is_empty()).then_some(self.commit),
81            "dirty": self.dirty,
82            "official": self.is_official(),
83        })
84    }
85}
86
87/// The Engine's own stamp (its build script re-runs on Engine sources and
88/// `HEAD` only).
89const ENGINE_BUILD: BuildInfo = BuildInfo {
90    kind: env!("LEANCTX_BUILD_KIND"),
91    commit: env!("LEANCTX_BUILD_COMMIT"),
92    dirty: matches!(env!("LEANCTX_BUILD_DIRTY").as_bytes(), b"1"),
93};
94
95/// The binary's commit and dirty flag, stamped over every crate's sources.
96static BINARY_STAMP: std::sync::OnceLock<(&'static str, bool)> = std::sync::OnceLock::new();
97
98/// Called once by the `lean-ctx` binary at start with its own compile-time
99/// stamp (`crates/lean-ctx/build.rs`): the binary links every crate, so its
100/// commit and dirty flag describe the whole build. An empty `commit` (a
101/// packaged crate, no git) keeps the Engine's stamp. An `official` build
102/// keeps its release commit.
103pub fn set_binary_stamp(commit: &'static str, dirty: bool) {
104    if !commit.is_empty() {
105        let _ = BINARY_STAMP.set((commit, dirty));
106    }
107}
108
109/// The build stamped into this binary.
110pub fn build() -> BuildInfo {
111    match BINARY_STAMP.get() {
112        Some(&(commit, dirty)) if !ENGINE_BUILD.is_official() => BuildInfo {
113            commit,
114            dirty,
115            ..ENGINE_BUILD
116        },
117        _ => ENGINE_BUILD,
118    }
119}
120
121pub struct IntegrityReport {
122    pub seed_ok: bool,
123    pub origin_ok: bool,
124    pub repo: &'static str,
125    pub pkg_name: &'static str,
126    pub version: &'static str,
127    pub build: BuildInfo,
128}
129
130pub fn check() -> IntegrityReport {
131    IntegrityReport {
132        seed_ok: verify_integrity(),
133        origin_ok: is_official_origin(),
134        repo: ORIGIN_REPO,
135        pkg_name: ORIGIN_NAME,
136        version: PKG_VERSION,
137        build: build(),
138    }
139}
140
141/// `lean-ctx --version --json`.
142pub fn version_json() -> serde_json::Value {
143    let report = check();
144    serde_json::json!({
145        "name": "lean-ctx",
146        "version": report.version,
147        "repository": report.repo,
148        "unmodified": report.seed_ok && report.origin_ok,
149        "build": report.build.to_json(),
150    })
151}
152
153pub fn origin_line() -> String {
154    let report = check();
155    if report.seed_ok && report.origin_ok && report.build.is_official() {
156        format!("lean-ctx {} (official, {})", report.version, report.repo)
157    } else if report.seed_ok && report.origin_ok {
158        format!("lean-ctx {} ({})", report.version, report.build.label())
159    } else {
160        format!(
161            "WARNING: Modified redistribution detected. \
162             Official builds: https://github.com/yvgude/lean-ctx \
163             (pkg={}, repo={})",
164            report.pkg_name, report.repo
165        )
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn integrity_passes_in_official_build() {
175        assert!(verify_integrity());
176    }
177
178    #[test]
179    fn seed_hash_is_deterministic() {
180        let h1 = compute_seed_hash("lean-ctx");
181        let h2 = compute_seed_hash("lean-ctx");
182        assert_eq!(h1, h2);
183    }
184
185    #[test]
186    fn tampered_seed_fails() {
187        let tampered = compute_seed_hash("better-ctx");
188        assert_ne!(tampered, expected_hash());
189    }
190
191    #[test]
192    fn origin_is_official() {
193        assert!(is_official_origin());
194    }
195
196    #[test]
197    fn origin_line_contains_version() {
198        let line = origin_line();
199        assert!(line.contains(env!("CARGO_PKG_VERSION")));
200    }
201
202    #[test]
203    fn build_kind_labels_name_only_release_builds_official() {
204        let sha = "f78aff8dc5a1b2c3d4e5f60718293a4b5c6d7e8f";
205        let b = |kind, commit, dirty| BuildInfo {
206            kind,
207            commit,
208            dirty,
209        };
210        assert_eq!(b("official", sha, false).label(), "official");
211        assert_eq!(b("dogfood", sha, false).label(), "dogfood f78aff8dc5");
212        assert_eq!(b("dev", sha, true).label(), "dev f78aff8dc5, dirty");
213        assert_eq!(b("source", "", false).label(), "source");
214        assert_eq!(b("dev", sha, false).to_json()["official"], false);
215        assert_eq!(
216            b("source", "", false).to_json()["commit"],
217            serde_json::Value::Null
218        );
219
220        // The stamped build: "official" in the line exactly when build.rs saw
221        // the release workflow (release.yml runs this suite too).
222        let this = build();
223        assert!(
224            matches!(this.kind, "official" | "dev" | "dogfood" | "source"),
225            "{this:?}"
226        );
227        assert!(
228            this.commit.is_empty() || this.commit.len() == 40,
229            "{this:?}"
230        );
231        assert_eq!(
232            origin_line().contains("(official,"),
233            this.is_official(),
234            "{}",
235            origin_line()
236        );
237        assert_eq!(version_json()["build"]["kind"], this.kind);
238    }
239}