Skip to main content

lean_ctx_protocol/
org_policy_verifier.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Hand-off between an endpoint and an organization-policy verifier, and the
4//! endpoint's record that an organization policy is required.
5//!
6//! An endpoint that finds an organization policy it cannot verify itself asks
7//! a verifier. This module holds only the shapes of that exchange and of the
8//! required-marker record, with the checks that need no secret and no rule:
9//! that an answer is the answer to *this* request, and that the marker only
10//! moves forward. It contains no policy rule (validity windows, grace,
11//! rollback, who may lift a requirement) and performs no cryptography.
12//!
13//! Interface contract, kept by the caller, not enforced by these types:
14//! - the endpoint accepts an answer only from a verifier it registered through
15//!   an authenticated channel, and verifies the envelope signature over
16//!   [`OrgPolicyVerifyResponseV1::signing_bytes`] with that verifier's key;
17//! - the endpoint generates the nonce for one request and never reuses it;
18//! - every failure — no verifier, a bad signature, an answer that does not
19//!   bind, a refusal — is treated as "policy cannot be established", which an
20//!   endpoint under a requirement answers by denying governed operations;
21//! - the marker is persisted atomically and read into the same snapshot whose
22//!   digests the request carries.
23
24use serde::{Deserialize, Serialize};
25use sha2::{Digest as _, Sha256};
26use std::cmp::Ordering;
27use std::fmt::Write as _;
28
29use crate::ValidationError;
30use crate::team_context::{
31    ContentDigest, Ed25519Signature, KeyId, NonceDigest, ReasonCode, RecordDigest, TeamId,
32    TeamTimestampV1, timestamp_cmp,
33};
34
35/// Schema id of the organization-policy artifact that needs a verifier.
36pub const ORG_POLICY_V2_ARTIFACT_SCHEMA_ID: &str = "leanctx.org-policy/v2";
37pub const ORG_POLICY_VERIFY_REQUEST_SCHEMA_ID: &str = "leanctx.org-policy-verify-request/v1";
38pub const ORG_POLICY_VERIFY_RESPONSE_SCHEMA_ID: &str = "leanctx.org-policy-verify-response/v1";
39pub const ORG_POLICY_VERIFY_REQUEST_DIGEST_DOMAIN: &str = "leanctx.org-policy-verify-request.v1\n";
40pub const ORG_POLICY_VERIFY_RESPONSE_SIGNATURE_DOMAIN: &str =
41    "leanctx.org-policy-verify-response.v1\n";
42pub const ORG_REQUIRED_MARKER_SCHEMA_ID: &str = "leanctx.org-required-marker/v1";
43pub const ORG_REQUIRED_MARKER_DIGEST_DOMAIN: &str = "leanctx.org-required-marker.v1\n";
44/// A resolved policy pack larger than this is refused.
45pub const MAX_ORG_POLICY_VERIFIED_PACK_BYTES: usize = 1024 * 1024;
46/// The marker keeps every accepted state; beyond this it refuses to advance.
47pub const MAX_ORG_REQUIRED_MARKER_ENTRIES: usize = 1024;
48/// Upper bound of a stored marker document.
49pub const MAX_ORG_REQUIRED_MARKER_BYTES: usize = 512 * 1024;
50
51const MAX_SEQUENCE: u64 = 9_007_199_254_740_991;
52
53fn hex_digest(domain: &str, bytes: &[u8]) -> String {
54    let mut hasher = Sha256::new();
55    hasher.update(domain.as_bytes());
56    hasher.update(bytes);
57    let mut value = String::from("sha256:");
58    for byte in hasher.finalize() {
59        write!(&mut value, "{byte:02x}").expect("writing to String cannot fail");
60    }
61    value
62}
63
64/// What the endpoint asks: verify the artifact of this snapshot, now.
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66#[serde(deny_unknown_fields)]
67pub struct OrgPolicyVerifyRequestV1 {
68    pub schema: String,
69    pub organization_id: TeamId,
70    /// SHA-256 of the installed artifact bytes.
71    pub artifact_digest: ContentDigest,
72    /// SHA-256 of the trust-store bytes; absent when no trust store exists.
73    pub trust_digest: Option<ContentDigest>,
74    /// SHA-256 of the required-marker bytes; absent when no marker exists.
75    pub marker_digest: Option<ContentDigest>,
76    /// Chosen by the endpoint for this one request.
77    pub nonce: NonceDigest,
78    pub requested_at: TeamTimestampV1,
79    /// An answer after this instant is not accepted.
80    pub deadline: TeamTimestampV1,
81}
82
83impl OrgPolicyVerifyRequestV1 {
84    pub fn validate(&self) -> Result<(), ValidationError> {
85        if self.schema != ORG_POLICY_VERIFY_REQUEST_SCHEMA_ID {
86            return Err(ValidationError::new(
87                "org policy verify request schema is unknown",
88            ));
89        }
90        if timestamp_cmp(&self.requested_at, &self.deadline) != Ordering::Less {
91            return Err(ValidationError::new(
92                "org policy verify request deadline is not after its request time",
93            ));
94        }
95        Ok(())
96    }
97
98    pub fn canonical_bytes(&self) -> Result<Vec<u8>, ValidationError> {
99        self.validate()?;
100        serde_json::to_vec(self)
101            .map_err(|_| ValidationError::new("org policy verify request cannot be encoded"))
102    }
103
104    pub fn digest(&self) -> Result<RecordDigest, ValidationError> {
105        RecordDigest::new(hex_digest(
106            ORG_POLICY_VERIFY_REQUEST_DIGEST_DOMAIN,
107            &self.canonical_bytes()?,
108        ))
109    }
110}
111
112/// The verifier's finding. `Enforce` carries the pack to apply; `NotRequired`
113/// names the signed unpin it rests on; `Refuse` names why nothing is permitted.
114#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
115#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
116pub enum OrgPolicyVerifyOutcomeV1 {
117    Enforce {
118        pack_toml: String,
119        policy_digest: RecordDigest,
120        sequence: u64,
121        /// The first instant at which this finding no longer holds
122        /// (exclusive). The verifier sets it from the policy it verified; an
123        /// endpoint must not use the answer at or after it, whatever the
124        /// request deadline says.
125        valid_until: TeamTimestampV1,
126    },
127    NotRequired {
128        unpin_digest: RecordDigest,
129        sequence: u64,
130    },
131    Refuse {
132        reason: ReasonCode,
133    },
134}
135
136/// The verifier's answer, bound to the request it answers.
137#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
138#[serde(deny_unknown_fields)]
139pub struct OrgPolicyVerifyResponseV1 {
140    pub schema: String,
141    pub request_digest: RecordDigest,
142    pub organization_id: TeamId,
143    pub artifact_digest: ContentDigest,
144    pub trust_digest: Option<ContentDigest>,
145    pub marker_digest: Option<ContentDigest>,
146    pub nonce: NonceDigest,
147    pub answered_at: TeamTimestampV1,
148    pub verifier_key_id: KeyId,
149    pub outcome: OrgPolicyVerifyOutcomeV1,
150}
151
152impl OrgPolicyVerifyResponseV1 {
153    pub fn validate(&self) -> Result<(), ValidationError> {
154        if self.schema != ORG_POLICY_VERIFY_RESPONSE_SCHEMA_ID {
155            return Err(ValidationError::new(
156                "org policy verify response schema is unknown",
157            ));
158        }
159        match &self.outcome {
160            OrgPolicyVerifyOutcomeV1::Enforce {
161                pack_toml,
162                sequence,
163                ..
164            } => {
165                if pack_toml.is_empty() || pack_toml.len() > MAX_ORG_POLICY_VERIFIED_PACK_BYTES {
166                    return Err(ValidationError::new(
167                        "org policy verify response pack size is out of range",
168                    ));
169                }
170                valid_sequence(*sequence)
171            }
172            OrgPolicyVerifyOutcomeV1::NotRequired { sequence, .. } => valid_sequence(*sequence),
173            OrgPolicyVerifyOutcomeV1::Refuse { .. } => Ok(()),
174        }
175    }
176
177    pub fn canonical_bytes(&self) -> Result<Vec<u8>, ValidationError> {
178        self.validate()?;
179        serde_json::to_vec(self)
180            .map_err(|_| ValidationError::new("org policy verify response cannot be encoded"))
181    }
182
183    /// The bytes the verifier signs and the endpoint verifies.
184    pub fn signing_bytes(&self) -> Result<Vec<u8>, ValidationError> {
185        let mut bytes = ORG_POLICY_VERIFY_RESPONSE_SIGNATURE_DOMAIN
186            .as_bytes()
187            .to_vec();
188        bytes.extend(self.canonical_bytes()?);
189        Ok(bytes)
190    }
191
192    /// Refuses unless this is the answer to `request`, given in time.
193    ///
194    /// Every bound element must match: the request digest, the organization,
195    /// the three snapshot digests and the nonce. The answer must lie inside
196    /// the request's window, and `now` must be neither before the answer nor
197    /// past the deadline (the caller keeps its own clock and deadline
198    /// monotonic; this check cannot). An `Enforce` finding is additionally
199    /// refused at or after its `valid_until`. A match
200    /// says nothing about who answered: the signature and the verifier's
201    /// registration are the caller's to check.
202    pub fn binds(
203        &self,
204        request: &OrgPolicyVerifyRequestV1,
205        now: &TeamTimestampV1,
206    ) -> Result<(), ValidationError> {
207        self.validate()?;
208        if self.request_digest != request.digest()?
209            || self.organization_id != request.organization_id
210            || self.artifact_digest != request.artifact_digest
211            || self.trust_digest != request.trust_digest
212            || self.marker_digest != request.marker_digest
213            || self.nonce != request.nonce
214        {
215            return Err(ValidationError::new(
216                "org policy verify response does not answer this request",
217            ));
218        }
219        // requested_at <= answered_at <= now <= deadline. A `now` before the
220        // answer is a clock that went backwards or an answer from the future;
221        // neither is accepted.
222        if timestamp_cmp(&self.answered_at, &request.requested_at) == Ordering::Less
223            || timestamp_cmp(&self.answered_at, &request.deadline) == Ordering::Greater
224            || timestamp_cmp(now, &self.answered_at) == Ordering::Less
225            || timestamp_cmp(now, &request.deadline) == Ordering::Greater
226        {
227            return Err(ValidationError::new(
228                "org policy verify response is outside the request window",
229            ));
230        }
231        // The finding's own limit is exclusive and independent of the request
232        // deadline: an answer signed while the policy held is not usable once
233        // it no longer does.
234        if let OrgPolicyVerifyOutcomeV1::Enforce { valid_until, .. } = &self.outcome
235            && (timestamp_cmp(&self.answered_at, valid_until) != Ordering::Less
236                || timestamp_cmp(now, valid_until) != Ordering::Less)
237        {
238            return Err(ValidationError::new(
239                "org policy verify response is past the validity of its finding",
240            ));
241        }
242        Ok(())
243    }
244}
245
246/// A response with the verifier's signature over its signing bytes.
247#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
248#[serde(deny_unknown_fields)]
249pub struct OrgPolicyVerifyEnvelopeV1 {
250    pub response: OrgPolicyVerifyResponseV1,
251    pub signature: Ed25519Signature,
252}
253
254fn valid_sequence(sequence: u64) -> Result<(), ValidationError> {
255    if sequence == 0 || sequence > MAX_SEQUENCE {
256        return Err(ValidationError::new("org policy sequence is out of range"));
257    }
258    Ok(())
259}
260
261/// One accepted state of the requirement.
262#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
263#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
264pub enum OrgRequiredMarkerStateV1 {
265    /// A policy with this digest was accepted as required.
266    Required { policy_digest: RecordDigest },
267    /// The requirement was lifted by the signed unpin with this digest.
268    Unpinned { unpin_digest: RecordDigest },
269}
270
271#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
272#[serde(deny_unknown_fields)]
273pub struct OrgRequiredMarkerEntryV1 {
274    pub sequence: u64,
275    pub recorded_at: TeamTimestampV1,
276    pub state: OrgRequiredMarkerStateV1,
277}
278
279/// The endpoint's durable record that an organization policy is, or was,
280/// required. It only grows: a lifted requirement stays on record.
281#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
282#[serde(deny_unknown_fields)]
283pub struct OrgRequiredMarkerV1 {
284    pub schema: String,
285    pub organization_id: TeamId,
286    /// Oldest first; the last entry is the current state.
287    pub entries: Vec<OrgRequiredMarkerEntryV1>,
288}
289
290/// What [`OrgRequiredMarkerV1::advance`] did.
291#[derive(Debug, Clone, Copy, PartialEq, Eq)]
292pub enum OrgRequiredMarkerAdvance {
293    /// The entry was appended; the marker must be persisted before it is used.
294    Appended,
295    /// The entry equals the current state; nothing changed.
296    Unchanged,
297}
298
299impl OrgRequiredMarkerV1 {
300    /// A new marker with its first accepted state.
301    pub fn first(
302        organization_id: TeamId,
303        entry: OrgRequiredMarkerEntryV1,
304    ) -> Result<Self, ValidationError> {
305        let marker = Self {
306            schema: ORG_REQUIRED_MARKER_SCHEMA_ID.into(),
307            organization_id,
308            entries: vec![entry],
309        };
310        marker.validate()?;
311        Ok(marker)
312    }
313
314    pub fn validate(&self) -> Result<(), ValidationError> {
315        if self.schema != ORG_REQUIRED_MARKER_SCHEMA_ID {
316            return Err(ValidationError::new(
317                "org required marker schema is unknown",
318            ));
319        }
320        if self.entries.is_empty() || self.entries.len() > MAX_ORG_REQUIRED_MARKER_ENTRIES {
321            return Err(ValidationError::new(
322                "org required marker entry count is out of range",
323            ));
324        }
325        valid_sequence(self.entries[0].sequence)?;
326        for pair in self.entries.windows(2) {
327            valid_sequence(pair[1].sequence)?;
328            if !follows(&pair[0], &pair[1]) {
329                return Err(ValidationError::new(
330                    "org required marker history does not move forward",
331                ));
332            }
333        }
334        Ok(())
335    }
336
337    /// The current state: the newest entry.
338    pub fn current(&self) -> &OrgRequiredMarkerEntryV1 {
339        self.entries
340            .last()
341            .expect("a validated marker has at least one entry")
342    }
343
344    /// Whether the newest state is a requirement.
345    pub fn is_required(&self) -> bool {
346        matches!(
347            self.current().state,
348            OrgRequiredMarkerStateV1::Required { .. }
349        )
350    }
351
352    /// Records the next accepted state for `organization_id`.
353    ///
354    /// Refuses another organization, a lower sequence, an equal sequence with
355    /// other content, and a requirement at the sequence of an unpin. The same
356    /// state again is `Unchanged`. Nothing is ever removed.
357    pub fn advance(
358        &mut self,
359        organization_id: &TeamId,
360        entry: OrgRequiredMarkerEntryV1,
361    ) -> Result<OrgRequiredMarkerAdvance, ValidationError> {
362        self.validate()?;
363        valid_sequence(entry.sequence)?;
364        if organization_id != &self.organization_id {
365            return Err(ValidationError::new(
366                "org required marker belongs to another organization",
367            ));
368        }
369        let current = self.current();
370        if entry.sequence == current.sequence && entry.state == current.state {
371            return Ok(OrgRequiredMarkerAdvance::Unchanged);
372        }
373        if !follows(current, &entry) {
374            return Err(ValidationError::new(
375                "org required marker refuses a state that does not move forward",
376            ));
377        }
378        if self.entries.len() >= MAX_ORG_REQUIRED_MARKER_ENTRIES {
379            return Err(ValidationError::new("org required marker history is full"));
380        }
381        self.entries.push(entry);
382        Ok(OrgRequiredMarkerAdvance::Appended)
383    }
384
385    pub fn canonical_bytes(&self) -> Result<Vec<u8>, ValidationError> {
386        self.validate()?;
387        serde_json::to_vec(self)
388            .map_err(|_| ValidationError::new("org required marker cannot be encoded"))
389    }
390
391    /// Parses a stored marker: bounded, strict, and byte-for-byte canonical.
392    pub fn parse(bytes: &[u8]) -> Result<Self, ValidationError> {
393        if bytes.len() > MAX_ORG_REQUIRED_MARKER_BYTES {
394            return Err(ValidationError::new("org required marker is too large"));
395        }
396        let marker: Self = serde_json::from_slice(bytes)
397            .map_err(|_| ValidationError::new("org required marker is malformed"))?;
398        if marker.canonical_bytes()? != bytes {
399            return Err(ValidationError::new(
400                "org required marker is not in canonical form",
401            ));
402        }
403        Ok(marker)
404    }
405
406    pub fn digest(&self) -> Result<RecordDigest, ValidationError> {
407        RecordDigest::new(hex_digest(
408            ORG_REQUIRED_MARKER_DIGEST_DOMAIN,
409            &self.canonical_bytes()?,
410        ))
411    }
412}
413
414/// Whether `next` may follow `previous`: a higher sequence always; the same
415/// sequence only to lift the requirement that sequence established.
416fn follows(previous: &OrgRequiredMarkerEntryV1, next: &OrgRequiredMarkerEntryV1) -> bool {
417    match next.sequence.cmp(&previous.sequence) {
418        Ordering::Greater => true,
419        Ordering::Less => false,
420        Ordering::Equal => matches!(
421            (&previous.state, &next.state),
422            (
423                OrgRequiredMarkerStateV1::Required { .. },
424                OrgRequiredMarkerStateV1::Unpinned { .. }
425            )
426        ),
427    }
428}
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433
434    fn content(fill: char) -> ContentDigest {
435        ContentDigest::new(format!("sha256:{}", fill.to_string().repeat(64))).unwrap()
436    }
437
438    fn record(fill: char) -> RecordDigest {
439        RecordDigest::new(format!("sha256:{}", fill.to_string().repeat(64))).unwrap()
440    }
441
442    fn at(value: &str) -> TeamTimestampV1 {
443        TeamTimestampV1::new(value).unwrap()
444    }
445
446    fn org(name: &str) -> TeamId {
447        TeamId::new(name).unwrap()
448    }
449
450    fn request() -> OrgPolicyVerifyRequestV1 {
451        OrgPolicyVerifyRequestV1 {
452            schema: ORG_POLICY_VERIFY_REQUEST_SCHEMA_ID.into(),
453            organization_id: org("acme"),
454            artifact_digest: content('a'),
455            trust_digest: Some(content('b')),
456            marker_digest: Some(content('c')),
457            nonce: NonceDigest::new(format!("sha256:{}", "d".repeat(64))).unwrap(),
458            requested_at: at("2026-10-10T09:00:00Z"),
459            deadline: at("2026-10-10T09:00:02Z"),
460        }
461    }
462
463    fn response(request: &OrgPolicyVerifyRequestV1) -> OrgPolicyVerifyResponseV1 {
464        OrgPolicyVerifyResponseV1 {
465            schema: ORG_POLICY_VERIFY_RESPONSE_SCHEMA_ID.into(),
466            request_digest: request.digest().unwrap(),
467            organization_id: request.organization_id.clone(),
468            artifact_digest: request.artifact_digest.clone(),
469            trust_digest: request.trust_digest.clone(),
470            marker_digest: request.marker_digest.clone(),
471            nonce: request.nonce.clone(),
472            answered_at: at("2026-10-10T09:00:01Z"),
473            verifier_key_id: KeyId::new("key:verifier-test").unwrap(),
474            outcome: OrgPolicyVerifyOutcomeV1::Enforce {
475                pack_toml: "name = \"floor\"\n".into(),
476                policy_digest: record('e'),
477                sequence: 7,
478                valid_until: at("2026-11-01T00:00:00Z"),
479            },
480        }
481    }
482
483    fn enforce_until(
484        request: &OrgPolicyVerifyRequestV1,
485        valid_until: &str,
486    ) -> OrgPolicyVerifyResponseV1 {
487        let mut response = response(request);
488        response.outcome = OrgPolicyVerifyOutcomeV1::Enforce {
489            pack_toml: "name = \"floor\"\n".into(),
490            policy_digest: record('e'),
491            sequence: 7,
492            valid_until: at(valid_until),
493        };
494        response
495    }
496
497    #[test]
498    fn an_enforce_finding_is_refused_at_and_after_its_own_limit() {
499        // Request 09:00:00–09:00:02, answered 09:00:01. The finding holds
500        // until 09:00:01.500, exclusive — inside the request deadline.
501        let request = request();
502        let limited = enforce_until(&request, "2026-10-10T09:00:01.500Z");
503        assert!(
504            limited
505                .binds(&request, &at("2026-10-10T09:00:01.499Z"))
506                .is_ok()
507        );
508        assert!(
509            limited
510                .binds(&request, &at("2026-10-10T09:00:01.500Z"))
511                .is_err(),
512            "the limit itself is outside"
513        );
514        assert!(
515            limited
516                .binds(&request, &at("2026-10-10T09:00:01.900Z"))
517                .is_err(),
518            "still before the request deadline, past the finding"
519        );
520        // A finding whose limit is not after the answer never held.
521        let already_over = enforce_until(&request, "2026-10-10T09:00:01Z");
522        assert!(
523            already_over
524                .binds(&request, &at("2026-10-10T09:00:01Z"))
525                .is_err()
526        );
527        // The limit is signed: changing it changes what the verifier signed.
528        assert_ne!(
529            limited.signing_bytes().unwrap(),
530            enforce_until(&request, "2026-10-10T09:00:02Z")
531                .signing_bytes()
532                .unwrap()
533        );
534    }
535
536    #[test]
537    fn an_answer_binds_only_the_request_it_was_given_for() {
538        let request = request();
539        let now = at("2026-10-10T09:00:01.500Z");
540        assert!(response(&request).binds(&request, &now).is_ok());
541
542        let mut other_org = response(&request);
543        other_org.organization_id = org("other");
544        assert!(other_org.binds(&request, &now).is_err());
545
546        let mut other_artifact = response(&request);
547        other_artifact.artifact_digest = content('f');
548        assert!(other_artifact.binds(&request, &now).is_err());
549
550        let mut no_trust = response(&request);
551        no_trust.trust_digest = None;
552        assert!(no_trust.binds(&request, &now).is_err());
553
554        let mut other_marker = response(&request);
555        other_marker.marker_digest = Some(content('f'));
556        assert!(other_marker.binds(&request, &now).is_err());
557
558        let mut replayed = response(&request);
559        replayed.nonce = NonceDigest::new(format!("sha256:{}", "9".repeat(64))).unwrap();
560        assert!(replayed.binds(&request, &now).is_err());
561
562        // An answer to a request that differs only in its deadline carries
563        // another request digest.
564        let mut later = request.clone();
565        later.deadline = at("2026-10-10T09:00:03Z");
566        assert!(response(&later).binds(&request, &now).is_err());
567    }
568
569    #[test]
570    fn an_answer_outside_the_window_is_refused() {
571        let request = request();
572        let in_time = at("2026-10-10T09:00:02Z");
573        assert!(response(&request).binds(&request, &in_time).is_ok());
574
575        let late_use = at("2026-10-10T09:00:02.001Z");
576        assert!(response(&request).binds(&request, &late_use).is_err());
577
578        let mut answered_late = response(&request);
579        answered_late.answered_at = at("2026-10-10T09:00:03Z");
580        assert!(answered_late.binds(&request, &in_time).is_err());
581
582        let mut answered_early = response(&request);
583        answered_early.answered_at = at("2026-10-10T08:59:59Z");
584        assert!(answered_early.binds(&request, &in_time).is_err());
585
586        // The answer (09:00:01) lies in the window, but the endpoint's clock
587        // reads a moment before it: before the request, and between the
588        // request and the answer.
589        let before_request = at("2026-10-10T08:59:59Z");
590        assert!(response(&request).binds(&request, &before_request).is_err());
591        let before_answer = at("2026-10-10T09:00:00.999Z");
592        assert!(response(&request).binds(&request, &before_answer).is_err());
593        let at_answer = at("2026-10-10T09:00:01Z");
594        assert!(response(&request).binds(&request, &at_answer).is_ok());
595    }
596
597    #[test]
598    fn request_and_response_refuse_malformed_content() {
599        let mut inverted = request();
600        inverted.deadline = inverted.requested_at.clone();
601        assert!(inverted.validate().is_err());
602
603        let request = request();
604        let mut empty_pack = response(&request);
605        empty_pack.outcome = OrgPolicyVerifyOutcomeV1::Enforce {
606            pack_toml: String::new(),
607            policy_digest: record('e'),
608            sequence: 7,
609            valid_until: at("2026-11-01T00:00:00Z"),
610        };
611        assert!(empty_pack.validate().is_err());
612
613        let mut zero = response(&request);
614        zero.outcome = OrgPolicyVerifyOutcomeV1::NotRequired {
615            unpin_digest: record('e'),
616            sequence: 0,
617        };
618        assert!(zero.validate().is_err());
619
620        let unknown = br#"{"schema":"leanctx.org-policy-verify-request/v1","extra":1}"#;
621        assert!(serde_json::from_slice::<OrgPolicyVerifyRequestV1>(unknown).is_err());
622    }
623
624    #[test]
625    fn signing_bytes_cover_the_outcome() {
626        let request = request();
627        let enforce = response(&request);
628        let mut refuse = response(&request);
629        refuse.outcome = OrgPolicyVerifyOutcomeV1::Refuse {
630            reason: ReasonCode::new("policy_expired").unwrap(),
631        };
632        assert_ne!(
633            enforce.signing_bytes().unwrap(),
634            refuse.signing_bytes().unwrap()
635        );
636        assert!(
637            enforce
638                .signing_bytes()
639                .unwrap()
640                .starts_with(ORG_POLICY_VERIFY_RESPONSE_SIGNATURE_DOMAIN.as_bytes())
641        );
642    }
643
644    fn required(sequence: u64, fill: char) -> OrgRequiredMarkerEntryV1 {
645        OrgRequiredMarkerEntryV1 {
646            sequence,
647            recorded_at: at("2026-10-10T09:00:00Z"),
648            state: OrgRequiredMarkerStateV1::Required {
649                policy_digest: record(fill),
650            },
651        }
652    }
653
654    fn unpinned(sequence: u64, fill: char) -> OrgRequiredMarkerEntryV1 {
655        OrgRequiredMarkerEntryV1 {
656            sequence,
657            recorded_at: at("2026-10-10T10:00:00Z"),
658            state: OrgRequiredMarkerStateV1::Unpinned {
659                unpin_digest: record(fill),
660            },
661        }
662    }
663
664    #[test]
665    fn the_marker_only_moves_forward() {
666        let acme = org("acme");
667        let mut marker = OrgRequiredMarkerV1::first(acme.clone(), required(3, 'a')).unwrap();
668        assert!(marker.is_required());
669
670        assert_eq!(
671            marker.advance(&acme, required(3, 'a')).unwrap(),
672            OrgRequiredMarkerAdvance::Unchanged
673        );
674        assert!(marker.advance(&acme, required(2, 'b')).is_err(), "lower");
675        assert!(
676            marker.advance(&acme, required(3, 'b')).is_err(),
677            "equal sequence, other content"
678        );
679        assert!(
680            marker.advance(&org("other"), required(4, 'b')).is_err(),
681            "another organization"
682        );
683        assert_eq!(marker.entries.len(), 1, "refusals change nothing");
684
685        assert_eq!(
686            marker.advance(&acme, required(4, 'b')).unwrap(),
687            OrgRequiredMarkerAdvance::Appended
688        );
689        assert_eq!(marker.current().sequence, 4);
690    }
691
692    #[test]
693    fn a_signed_unpin_keeps_the_history() {
694        let acme = org("acme");
695        let mut marker = OrgRequiredMarkerV1::first(acme.clone(), required(3, 'a')).unwrap();
696        assert_eq!(
697            marker.advance(&acme, unpinned(3, 'd')).unwrap(),
698            OrgRequiredMarkerAdvance::Appended
699        );
700        assert!(!marker.is_required());
701        assert_eq!(marker.entries.len(), 2, "the requirement stays on record");
702        assert_eq!(marker.entries[0], required(3, 'a'));
703
704        assert!(
705            marker.advance(&acme, required(3, 'a')).is_err(),
706            "the lifted requirement cannot be re-entered at its own sequence"
707        );
708        assert!(marker.advance(&acme, unpinned(2, 'd')).is_err());
709        assert_eq!(
710            marker.advance(&acme, required(4, 'b')).unwrap(),
711            OrgRequiredMarkerAdvance::Appended
712        );
713        assert!(marker.is_required());
714        assert_eq!(marker.entries.len(), 3);
715    }
716
717    #[test]
718    fn a_stored_marker_is_strict_and_canonical() {
719        let acme = org("acme");
720        let mut marker = OrgRequiredMarkerV1::first(acme.clone(), required(3, 'a')).unwrap();
721        marker.advance(&acme, unpinned(3, 'd')).unwrap();
722        let bytes = marker.canonical_bytes().unwrap();
723        assert_eq!(OrgRequiredMarkerV1::parse(&bytes).unwrap(), marker);
724        assert_ne!(
725            marker.digest().unwrap(),
726            OrgRequiredMarkerV1::first(acme, required(3, 'a'))
727                .unwrap()
728                .digest()
729                .unwrap()
730        );
731
732        let mut spaced = bytes.clone();
733        spaced.push(b' ');
734        assert!(OrgRequiredMarkerV1::parse(&spaced).is_err());
735        assert!(OrgRequiredMarkerV1::parse(b"{}").is_err());
736
737        // A stored history that goes backwards is refused as a whole.
738        let mut backwards = marker.clone();
739        backwards.entries.swap(0, 1);
740        assert!(backwards.validate().is_err());
741        assert!(serde_json::to_vec(&backwards).is_ok());
742        assert!(OrgRequiredMarkerV1::parse(&serde_json::to_vec(&backwards).unwrap()).is_err());
743    }
744
745    #[test]
746    fn a_full_history_refuses_to_advance() {
747        let acme = org("acme");
748        let mut marker = OrgRequiredMarkerV1::first(acme.clone(), required(1, 'a')).unwrap();
749        for sequence in 2..=MAX_ORG_REQUIRED_MARKER_ENTRIES as u64 {
750            marker.advance(&acme, required(sequence, 'a')).unwrap();
751        }
752        assert!(
753            marker
754                .advance(
755                    &acme,
756                    required(MAX_ORG_REQUIRED_MARKER_ENTRIES as u64 + 1, 'a')
757                )
758                .is_err()
759        );
760    }
761}