1use chrono::{DateTime, Utc};
4use cortex_core::{
5 revalidate_temporal_authority, KeyLifecycleState, PolicyContribution, PolicyDecision,
6 PolicyOutcome, TemporalAuthorityEvidence, TemporalAuthorityReport, TrustTier,
7};
8use rusqlite::{params, OptionalExtension, Row};
9
10use crate::{Pool, StoreError, StoreResult};
11
12pub const KEY_STATE_ATTESTED_BY_OPERATOR_RULE_ID: &str = "authority.key_state.attested_by_operator";
15pub const KEY_STATE_TRUST_TIER_GATE_RULE_ID: &str = "authority.key_state.trust_tier_gate";
18pub const PRINCIPAL_STATE_ATTESTED_BY_OPERATOR_RULE_ID: &str =
21 "authority.principal_state.attested_by_operator";
22pub const PRINCIPAL_STATE_TRUST_TIER_GATE_RULE_ID: &str =
25 "authority.principal_state.trust_tier_gate";
26pub const PRINCIPAL_STATE_TIER_PROMOTION_ATTESTATION_RULE_ID: &str =
30 "authority.principal_state.tier_promotion_attestation";
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct KeyTimelineRecord {
35 pub key_id: String,
37 pub principal_id: String,
39 pub state: KeyLifecycleState,
41 pub effective_at: DateTime<Utc>,
43 pub reason: Option<String>,
45 pub audit_ref: Option<String>,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct PrincipalTimelineRecord {
52 pub principal_id: String,
54 pub trust_tier: TrustTier,
56 pub effective_at: DateTime<Utc>,
58 pub trust_review_due_at: Option<DateTime<Utc>>,
60 pub removed_at: Option<DateTime<Utc>>,
62 pub audit_ref: Option<String>,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct TemporalAuthorityQuery {
69 pub key_id: String,
71 pub event_time: DateTime<Utc>,
73 pub now: DateTime<Utc>,
75 pub minimum_trust_tier: TrustTier,
77}
78
79#[derive(Debug)]
81pub struct AuthorityRepo<'a> {
82 pool: &'a Pool,
83}
84
85impl<'a> AuthorityRepo<'a> {
86 #[must_use]
88 pub const fn new(pool: &'a Pool) -> Self {
89 Self { pool }
90 }
91
92 pub fn append_key_state(
114 &self,
115 record: &KeyTimelineRecord,
116 policy: &PolicyDecision,
117 ) -> StoreResult<()> {
118 require_policy_final_outcome(policy, "authority.key_state")?;
119 require_contributor_rule(policy, KEY_STATE_ATTESTED_BY_OPERATOR_RULE_ID)?;
120 require_contributor_rule(policy, KEY_STATE_TRUST_TIER_GATE_RULE_ID)?;
121 require_attestation_not_break_glassed(
122 policy,
123 KEY_STATE_ATTESTED_BY_OPERATOR_RULE_ID,
124 "authority.key_state",
125 )?;
126
127 if matches!(record.state, KeyLifecycleState::Active) {
128 self.reject_active_key_for_undertrust_principal(record)?;
129 }
130
131 self.pool.execute(
132 "INSERT INTO authority_key_timeline (
133 key_id, principal_id, state, effective_at, reason, audit_ref
134 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6);",
135 params![
136 record.key_id,
137 record.principal_id,
138 key_state_wire(record.state),
139 record.effective_at.to_rfc3339(),
140 record.reason,
141 record.audit_ref,
142 ],
143 )?;
144 Ok(())
145 }
146
147 pub fn append_principal_state(
156 &self,
157 record: &PrincipalTimelineRecord,
158 policy: &PolicyDecision,
159 ) -> StoreResult<()> {
160 require_policy_final_outcome(policy, "authority.principal_state")?;
161 require_contributor_rule(policy, PRINCIPAL_STATE_ATTESTED_BY_OPERATOR_RULE_ID)?;
162 require_contributor_rule(policy, PRINCIPAL_STATE_TRUST_TIER_GATE_RULE_ID)?;
163 require_contributor_rule(policy, PRINCIPAL_STATE_TIER_PROMOTION_ATTESTATION_RULE_ID)?;
164 require_attestation_not_break_glassed(
165 policy,
166 PRINCIPAL_STATE_ATTESTED_BY_OPERATOR_RULE_ID,
167 "authority.principal_state",
168 )?;
169 require_attestation_not_break_glassed(
170 policy,
171 PRINCIPAL_STATE_TIER_PROMOTION_ATTESTATION_RULE_ID,
172 "authority.principal_state",
173 )?;
174
175 self.pool.execute(
176 "INSERT INTO authority_principal_timeline (
177 principal_id, trust_tier, effective_at, trust_review_due_at, removed_at, audit_ref
178 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6);",
179 params![
180 record.principal_id,
181 trust_tier_wire(record.trust_tier),
182 record.effective_at.to_rfc3339(),
183 record.trust_review_due_at.map(|value| value.to_rfc3339()),
184 record.removed_at.map(|value| value.to_rfc3339()),
185 record.audit_ref,
186 ],
187 )?;
188 Ok(())
189 }
190
191 fn reject_active_key_for_undertrust_principal(
192 &self,
193 record: &KeyTimelineRecord,
194 ) -> StoreResult<()> {
195 let current_trust = self.principal_state_at(&record.principal_id, record.effective_at)?;
196 match current_trust {
197 Some(state) if state.trust_tier < TrustTier::Verified => {
198 Err(StoreError::Validation(format!(
199 "authority.key_state preflight: refuse to activate key `{}` for principal `{}` while current trust tier `{:?}` is below `Verified`",
200 record.key_id, record.principal_id, state.trust_tier,
201 )))
202 }
203 None => Err(StoreError::Validation(format!(
205 "authority.key_state preflight: refuse to activate key `{}` for principal `{}` without a current trust tier row",
206 record.key_id, record.principal_id,
207 ))),
208 Some(_) => Ok(()),
209 }
210 }
211
212 pub fn revalidate(
214 &self,
215 query: &TemporalAuthorityQuery,
216 ) -> StoreResult<TemporalAuthorityReport> {
217 let key_at_event = self.key_state_at(&query.key_id, query.event_time)?;
218 let current_key_state = self.key_state_at(&query.key_id, query.now)?;
219 let key_activated_at = self.key_state_effective_at(
220 &query.key_id,
221 KeyLifecycleState::Active,
222 query.event_time,
223 )?;
224 let key_retired_at =
225 self.key_state_effective_at(&query.key_id, KeyLifecycleState::Retired, query.now)?;
226 let key_revoked_at =
227 self.key_state_effective_at(&query.key_id, KeyLifecycleState::Revoked, query.now)?;
228 let principal_id = key_at_event
229 .as_ref()
230 .or(current_key_state.as_ref())
231 .map(|record| record.principal_id.clone());
232 let trust_at_event = match principal_id.as_deref() {
233 Some(principal_id) => self.principal_state_at(principal_id, query.event_time)?,
234 None => None,
235 };
236 let current_trust = match principal_id.as_deref() {
237 Some(principal_id) => self.principal_state_at(principal_id, query.now)?,
238 None => None,
239 };
240
241 Ok(revalidate_temporal_authority(TemporalAuthorityEvidence {
242 key_id: query.key_id.clone(),
243 principal_id,
244 event_time: query.event_time,
245 now: query.now,
246 key_activated_at,
247 key_retired_at,
248 key_revoked_at,
249 trust_tier_at_event_time: trust_at_event.as_ref().map(|record| record.trust_tier),
250 current_trust_tier: current_trust.as_ref().map(|record| record.trust_tier),
251 current_trust_tier_effective_at: current_trust
252 .as_ref()
253 .map(|record| record.effective_at),
254 minimum_trust_tier: query.minimum_trust_tier,
255 principal_removed_at: current_trust.as_ref().and_then(|record| record.removed_at),
256 trust_review_due_at: current_trust
257 .as_ref()
258 .and_then(|record| record.trust_review_due_at),
259 }))
260 }
261
262 fn key_state_at(
263 &self,
264 key_id: &str,
265 at: DateTime<Utc>,
266 ) -> StoreResult<Option<KeyTimelineRecord>> {
267 let row = self
268 .pool
269 .query_row(
270 "SELECT key_id, principal_id, state, effective_at, reason, audit_ref
271 FROM authority_key_timeline
272 WHERE key_id = ?1 AND effective_at <= ?2
273 ORDER BY effective_at DESC, state DESC
274 LIMIT 1;",
275 params![key_id, at.to_rfc3339()],
276 key_timeline_row,
277 )
278 .optional()?;
279
280 row.map(TryInto::try_into).transpose()
281 }
282
283 fn key_state_effective_at(
284 &self,
285 key_id: &str,
286 state: KeyLifecycleState,
287 at: DateTime<Utc>,
288 ) -> StoreResult<Option<DateTime<Utc>>> {
289 let value = self
290 .pool
291 .query_row(
292 "SELECT effective_at
293 FROM authority_key_timeline
294 WHERE key_id = ?1 AND state = ?2 AND effective_at <= ?3
295 ORDER BY effective_at DESC
296 LIMIT 1;",
297 params![key_id, key_state_wire(state), at.to_rfc3339()],
298 |row| row.get::<_, String>(0),
299 )
300 .optional()?;
301
302 value.as_deref().map(parse_utc).transpose()
303 }
304
305 fn principal_state_at(
306 &self,
307 principal_id: &str,
308 at: DateTime<Utc>,
309 ) -> StoreResult<Option<PrincipalTimelineRecord>> {
310 let row = self
311 .pool
312 .query_row(
313 "SELECT principal_id, trust_tier, effective_at, trust_review_due_at, removed_at, audit_ref
314 FROM authority_principal_timeline
315 WHERE principal_id = ?1 AND effective_at <= ?2
316 ORDER BY effective_at DESC
317 LIMIT 1;",
318 params![principal_id, at.to_rfc3339()],
319 principal_timeline_row,
320 )
321 .optional()?;
322
323 row.map(TryInto::try_into).transpose()
324 }
325}
326
327#[derive(Debug)]
328struct KeyTimelineRow {
329 key_id: String,
330 principal_id: String,
331 state: String,
332 effective_at: String,
333 reason: Option<String>,
334 audit_ref: Option<String>,
335}
336
337fn key_timeline_row(row: &Row<'_>) -> rusqlite::Result<KeyTimelineRow> {
338 Ok(KeyTimelineRow {
339 key_id: row.get(0)?,
340 principal_id: row.get(1)?,
341 state: row.get(2)?,
342 effective_at: row.get(3)?,
343 reason: row.get(4)?,
344 audit_ref: row.get(5)?,
345 })
346}
347
348impl TryFrom<KeyTimelineRow> for KeyTimelineRecord {
349 type Error = StoreError;
350
351 fn try_from(row: KeyTimelineRow) -> StoreResult<Self> {
352 Ok(Self {
353 key_id: row.key_id,
354 principal_id: row.principal_id,
355 state: parse_key_state(&row.state)?,
356 effective_at: parse_utc(&row.effective_at)?,
357 reason: row.reason,
358 audit_ref: row.audit_ref,
359 })
360 }
361}
362
363#[derive(Debug)]
364struct PrincipalTimelineRow {
365 principal_id: String,
366 trust_tier: String,
367 effective_at: String,
368 trust_review_due_at: Option<String>,
369 removed_at: Option<String>,
370 audit_ref: Option<String>,
371}
372
373fn principal_timeline_row(row: &Row<'_>) -> rusqlite::Result<PrincipalTimelineRow> {
374 Ok(PrincipalTimelineRow {
375 principal_id: row.get(0)?,
376 trust_tier: row.get(1)?,
377 effective_at: row.get(2)?,
378 trust_review_due_at: row.get(3)?,
379 removed_at: row.get(4)?,
380 audit_ref: row.get(5)?,
381 })
382}
383
384impl TryFrom<PrincipalTimelineRow> for PrincipalTimelineRecord {
385 type Error = StoreError;
386
387 fn try_from(row: PrincipalTimelineRow) -> StoreResult<Self> {
388 Ok(Self {
389 principal_id: row.principal_id,
390 trust_tier: parse_trust_tier(&row.trust_tier)?,
391 effective_at: parse_utc(&row.effective_at)?,
392 trust_review_due_at: row
393 .trust_review_due_at
394 .as_deref()
395 .map(parse_utc)
396 .transpose()?,
397 removed_at: row.removed_at.as_deref().map(parse_utc).transpose()?,
398 audit_ref: row.audit_ref,
399 })
400 }
401}
402
403fn require_policy_final_outcome(policy: &PolicyDecision, surface: &str) -> StoreResult<()> {
404 match policy.final_outcome {
405 PolicyOutcome::Allow | PolicyOutcome::Warn | PolicyOutcome::BreakGlass => Ok(()),
406 PolicyOutcome::Quarantine | PolicyOutcome::Reject => Err(StoreError::Validation(format!(
407 "{surface} preflight: composed policy outcome {:?} blocks authority-root mutation",
408 policy.final_outcome,
409 ))),
410 }
411}
412
413fn require_contributor_rule(policy: &PolicyDecision, rule_id: &str) -> StoreResult<()> {
414 let contains_rule = policy
415 .contributing
416 .iter()
417 .chain(policy.discarded.iter())
418 .any(|contribution| contribution.rule_id.as_str() == rule_id);
419 if contains_rule {
420 Ok(())
421 } else {
422 Err(StoreError::Validation(format!(
423 "policy decision missing required contributor `{rule_id}`; caller skipped ADR 0026 composition",
424 )))
425 }
426}
427
428fn require_attestation_not_break_glassed(
429 policy: &PolicyDecision,
430 rule_id: &str,
431 surface: &str,
432) -> StoreResult<()> {
433 let attestation = policy
437 .contributing
438 .iter()
439 .chain(policy.discarded.iter())
440 .find(|contribution| contribution.rule_id.as_str() == rule_id)
441 .ok_or_else(|| {
442 StoreError::Validation(format!(
443 "{surface} preflight: required attestation contributor `{rule_id}` is absent from the policy decision",
444 ))
445 })?;
446 if attestation.outcome == PolicyOutcome::Allow {
447 Ok(())
448 } else {
449 Err(StoreError::Validation(format!(
450 "{surface} preflight: attestation contributor `{rule_id}` returned {:?}; ADR 0026 §4 forbids BreakGlass substituting for attestation",
451 attestation.outcome,
452 )))
453 }
454}
455
456#[must_use]
466pub fn key_state_policy_decision_test_allow() -> PolicyDecision {
467 use cortex_core::compose_policy_outcomes;
468 compose_policy_outcomes(
469 vec![
470 PolicyContribution::new(
471 KEY_STATE_ATTESTED_BY_OPERATOR_RULE_ID,
472 PolicyOutcome::Allow,
473 "test fixture: operator attestation present",
474 )
475 .expect("static test contribution is valid"),
476 PolicyContribution::new(
477 KEY_STATE_TRUST_TIER_GATE_RULE_ID,
478 PolicyOutcome::Allow,
479 "test fixture: trust tier gate satisfied",
480 )
481 .expect("static test contribution is valid"),
482 ],
483 None,
484 )
485}
486
487#[must_use]
493pub fn principal_state_policy_decision_test_allow() -> PolicyDecision {
494 use cortex_core::compose_policy_outcomes;
495 compose_policy_outcomes(
496 vec![
497 PolicyContribution::new(
498 PRINCIPAL_STATE_ATTESTED_BY_OPERATOR_RULE_ID,
499 PolicyOutcome::Allow,
500 "test fixture: operator attestation present",
501 )
502 .expect("static test contribution is valid"),
503 PolicyContribution::new(
504 PRINCIPAL_STATE_TRUST_TIER_GATE_RULE_ID,
505 PolicyOutcome::Allow,
506 "test fixture: trust tier gate satisfied",
507 )
508 .expect("static test contribution is valid"),
509 PolicyContribution::new(
510 PRINCIPAL_STATE_TIER_PROMOTION_ATTESTATION_RULE_ID,
511 PolicyOutcome::Allow,
512 "test fixture: tier promotion attestation present",
513 )
514 .expect("static test contribution is valid"),
515 ],
516 None,
517 )
518}
519
520fn parse_utc(value: &str) -> StoreResult<DateTime<Utc>> {
521 Ok(DateTime::parse_from_rfc3339(value)?.with_timezone(&Utc))
522}
523
524fn key_state_wire(state: KeyLifecycleState) -> &'static str {
525 match state {
526 KeyLifecycleState::Active => "active",
527 KeyLifecycleState::Retired => "retired",
528 KeyLifecycleState::Revoked => "revoked",
529 }
530}
531
532fn parse_key_state(value: &str) -> StoreResult<KeyLifecycleState> {
533 match value {
534 "active" => Ok(KeyLifecycleState::Active),
535 "retired" => Ok(KeyLifecycleState::Retired),
536 "revoked" => Ok(KeyLifecycleState::Revoked),
537 other => Err(StoreError::Validation(format!(
538 "unknown key lifecycle state `{other}`"
539 ))),
540 }
541}
542
543fn trust_tier_wire(tier: TrustTier) -> &'static str {
544 match tier {
545 TrustTier::Untrusted => "untrusted",
546 TrustTier::Observed => "observed",
547 TrustTier::Verified => "verified",
548 TrustTier::Operator => "operator",
549 }
550}
551
552fn parse_trust_tier(value: &str) -> StoreResult<TrustTier> {
553 match value {
554 "untrusted" => Ok(TrustTier::Untrusted),
555 "observed" => Ok(TrustTier::Observed),
556 "verified" => Ok(TrustTier::Verified),
557 "operator" => Ok(TrustTier::Operator),
558 other => Err(StoreError::Validation(format!(
559 "unknown trust tier `{other}`"
560 ))),
561 }
562}