feat(signature): surface an audit-only signing-regression finding on drift from the repo baseline (JEF-264)#139
Merged
thejefflarson merged 1 commit intoJul 1, 2026
Conversation
…drift from the repo baseline (JEF-264)
A repo with an established signed history suddenly serving unsigned/invalid,
or signed by a NEW identity, is the push-access-compromise signal — invisible
until now even with the JEF-263 baseline. This adds a pure, deterministic drift
classifier over (baseline, fresh posture) and surfaces a loud, audit-only
signing-regression finding on drift, still admitting (shadow invariant).
- Classifier (engine/signing_drift.rs): pure Continuous / Regression{Unsigned |
Invalid | IdentityChange} / NewRepo over (SigningBaseline, SigningPosture),
reading the baseline's `established` flag for reduced-intensity on cold
baselines. Never a clock/IO — exhaustively unit-tested.
- Sweep wiring: classify each observed posture against the CURRENT baseline
BEFORE learning, and record a regression finding onto the same admission log
(SigningRegression/<repo>, decision `allow` — never denies). No egress; not
journaled, rebuilt per pass.
- Render: a loud "signing regression" banner on the repo group (breach rail +
● glyph + word, lexically distinct from calm "not signed"), stating
before→after with BOTH identities in FULL. Every untrusted Fulcio SAN escaped
via maud interpolation only (no PreEscaped, no class= from untrusted text).
- Status-strip honesty: established regressions count toward breach, cold ones
toward uncertain — both forbid green — wired WITHOUT the reachability
Decision/Finding pipeline, via SigningRegressionCounts folded into the strip
on every tab.
- Split props.rs into props/{mod,signing}.rs to stay under the 1,000-line cap.
Tests: classifier (12, pure), sweep detection (6: →unsigned/→invalid/identity/
normal-redeploy-none/cold-start-none/cold-reduced), inventory mapping + counts
(8), regression render + crafted-identity escaping (3), strip honesty (2).
Closes JEF-264
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A repo with an established signed history that suddenly serves an unsigned/invalid image — or one signed by a new identity — is the push-access-compromise signal, and was invisible until now even with the JEF-263 baseline. This surfaces a loud, audit-only signing-regression finding on drift from the baseline. Still admits (shadow invariant, ADR-0016); enforcement (JEF-265) and Rekor history (JEF-266) stay out of scope.
Classifier design (
engine/src/engine/signing_drift.rs)Pure, deterministic
classify(Option<&SigningBaseline>, &SigningPosture) -> SigningDrift:Continuous— signed by a known identity (normal redeploy, even a new digest), or a transient/unknown state with no history to regress against.NewRepo— first signed sight of a never-seen repo (TOFU cold start; baseline recorded, no finding).Regression { kind, established }—Unsigned/Invalid/IdentityChange{new_identity,new_issuer}.establishedis read straight from the baseline (JEF-263's 24h flag), so the classifier stays clock-free and exhaustively unit-testable.Rules honored: signed→unsigned/invalid on an established repo ⇒ regression; new signer ⇒ a distinct regression; known-identity new digest ⇒ no finding; cold-start ⇒ no regression; regression against a cold baseline ⇒ reduced intensity (
established:false).Status-strip honesty model
Regressions feed the strip without the reachability
Decision/Findingpipeline. The sweep records each regression onto the admission-decision log (SigningRegression/<repo>, decisionallow);DashboardStatederives(established, cold)counts from that log and folds them intoStatusStripPropson every tab viawith_signing_regressions. Established regressions count toward breach (block green and the calm "watching" reading); cold regressions map to uncertain (block green, read as watching). A standing regression can never show green.Render
A loud
signing regressionbanner on the repo group (breach rail + ● glyph + word, lexically distinct from calm "not signed"), stating before→after with both identities in full. Every untrusted Fulcio SAN is emitted via maud interpolation only — noPreEscaped, no markup concatenation, anddata-regressionuses the fixed kind token, never identity text.Tests
Gates (all green)
cargo fmt·cargo build·cargo build --example dashboard_preview·cargo clippy --all-targets(0 warnings) ·cargo test(448 unit + guards, incl. the 1,000-line file-size guard).Judgment calls / scope notes
signature=regression-<kind>-<strength>,reason=<after> | before: <ids>) and parsed back in the view_model — the same string idiom the existing signing-inventory mapper already uses — so no new state handle or baseline access at render time.props.rspast the 1,000-line hard cap, so the signing-inventory presentation types were moved intoprops/signing.rs(re-exported flat). Required by the change, not opportunistic.Closes JEF-264
🤖 Generated with Claude Code