cortex_core/
source_attestation.rs1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9use crate::Attestation;
10
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(tag = "state", content = "value", rename_all = "snake_case")]
18pub enum SourceAttestation {
19 Verified(Attestation),
21 LegacyUnattested {
23 imported_at: DateTime<Utc>,
25 original_recorded_at: DateTime<Utc>,
27 },
28 Missing,
30}
31
32impl SourceAttestation {
33 #[must_use]
35 pub const fn is_verified(&self) -> bool {
36 matches!(self, Self::Verified(_))
37 }
38
39 #[must_use]
42 pub const fn is_unattested(&self) -> bool {
43 matches!(self, Self::LegacyUnattested { .. } | Self::Missing)
44 }
45}
46
47#[cfg(test)]
48mod tests {
49 use super::*;
50 use std::fs;
51 use std::path::{Path, PathBuf};
52
53 use crate::{Attestation, Attestor, InMemoryAttestor};
54
55 fn attestation_fixture() -> Attestation {
56 let attestor = InMemoryAttestor::from_seed(&[7; 32]);
57 Attestation {
58 key_id: attestor.key_id().to_string(),
59 signature: attestor.sign(b"source-attestation-test").to_bytes(),
60 signed_at: "2026-05-04T12:00:00Z".parse().unwrap(),
61 }
62 }
63
64 #[test]
65 fn source_attestation_variants_round_trip() {
66 let imported_at = "2026-05-04T13:00:00Z".parse().unwrap();
67 let original_recorded_at = "2026-05-04T11:59:00Z".parse().unwrap();
68 let variants = [
69 SourceAttestation::Verified(attestation_fixture()),
70 SourceAttestation::LegacyUnattested {
71 imported_at,
72 original_recorded_at,
73 },
74 SourceAttestation::Missing,
75 ];
76
77 for variant in variants {
78 let json = serde_json::to_string(&variant).unwrap();
79 let decoded: SourceAttestation = serde_json::from_str(&json).unwrap();
80 assert_eq!(decoded, variant);
81 }
82 }
83
84 #[test]
85 fn source_attestation_wire_strings_are_stable() {
86 let verified_json =
87 serde_json::to_value(SourceAttestation::Verified(attestation_fixture()))
88 .expect("verified variant serializes");
89 assert_eq!(verified_json["state"], "verified");
90
91 let legacy_json = serde_json::to_value(SourceAttestation::LegacyUnattested {
92 imported_at: "2026-05-04T13:00:00Z".parse().unwrap(),
93 original_recorded_at: "2026-05-04T11:59:00Z".parse().unwrap(),
94 })
95 .expect("legacy variant serializes");
96 assert_eq!(legacy_json["state"], "legacy_unattested");
97
98 let missing_json =
99 serde_json::to_value(SourceAttestation::Missing).expect("missing variant serializes");
100 assert_eq!(missing_json["state"], "missing");
101 }
102
103 #[test]
104 fn source_attestation_authority_helpers_separate_unattested_rows() {
105 assert!(SourceAttestation::Verified(attestation_fixture()).is_verified());
106 assert!(!SourceAttestation::Verified(attestation_fixture()).is_unattested());
107 assert!(SourceAttestation::LegacyUnattested {
108 imported_at: "2026-05-04T13:00:00Z".parse().unwrap(),
109 original_recorded_at: "2026-05-04T11:59:00Z".parse().unwrap(),
110 }
111 .is_unattested());
112 assert!(SourceAttestation::Missing.is_unattested());
113 }
114
115 #[test]
116 fn no_zero_signature_sentinel_in_codebase() {
117 let workspace = Path::new(env!("CARGO_MANIFEST_DIR"))
118 .parent()
119 .and_then(Path::parent)
120 .expect("cortex-core sits under crates/");
121 let mut files = Vec::new();
122 collect_rs_files(workspace, &mut files);
123 let compact_array = ["[0;", "64]"].join(" ");
124 let attestation_field = ["signature:", compact_array.as_str()].join(" ");
125
126 for file in files {
127 let text = fs::read_to_string(&file).expect("source file is readable");
128 assert!(
129 !text.contains(&attestation_field) && !text.contains(&compact_array),
130 "zero-signature attestation sentinel is forbidden by ADR 0018: {}",
131 file.display()
132 );
133 }
134 }
135
136 fn collect_rs_files(dir: &Path, files: &mut Vec<PathBuf>) {
137 let Ok(entries) = fs::read_dir(dir) else {
138 return;
139 };
140 for entry in entries {
141 let entry = entry.expect("directory entry is readable");
142 let path = entry.path();
143 if should_skip(&path) {
144 continue;
145 }
146 if path.is_dir() {
147 collect_rs_files(&path, files);
148 } else if path.extension().and_then(|ext| ext.to_str()) == Some("rs") {
149 files.push(path);
150 }
151 }
152 }
153
154 fn should_skip(path: &Path) -> bool {
155 path.file_name()
156 .and_then(|name| name.to_str())
157 .is_some_and(|name| {
158 matches!(
159 name,
160 "target" | ".git" | ".cargo" | "generated" | "fixtures"
161 )
162 })
163 }
164}