Skip to main content

max / makenotwork

10.3 KB · 307 lines History Blame Raw
1 //! The two documents that cross the boundary.
2
3 use crate::{BundleDigest, ContractError, GateRecord, Manifest, Provenance, Scope};
4
5 /// The version of this contract. Bumped when a change would make an older
6 /// reader misread a newer document, not when a field is added.
7 pub const CONTRACT_VERSION: u32 = 1;
8
9 /// What the builder hands over: these bytes, from this source, and here is what
10 /// I proved about them.
11 ///
12 /// Written **beside** the bundle, never inside it. Evidence names the digest of
13 /// what it vouches for, and the digest covers every file in the bundle, so
14 /// evidence stored inside would change the digest it names. Beyond the
15 /// circularity, the evaluator appends its own evidence later; if evidence lived
16 /// in the bundle, vouching for an artifact would alter the artifact's identity,
17 /// and the thing that passed would no longer be the thing that ships.
18 #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
19 pub struct ArtifactRecord {
20 pub contract: u32,
21 /// Which system produced this (`bento`).
22 pub producer: String,
23 /// Identity of the bundle. Always equal to `manifest.digest()`; carried
24 /// explicitly so a reader can check the manifest it was handed is the one
25 /// the digest was computed from.
26 pub digest: BundleDigest,
27 pub manifest: Manifest,
28 pub provenance: Provenance,
29 /// Gates the builder ran. Artifact-scoped by construction.
30 pub gates: Vec<GateRecord>,
31 }
32
33 impl ArtifactRecord {
34 pub fn new(
35 producer: impl Into<String>,
36 manifest: Manifest,
37 provenance: Provenance,
38 gates: Vec<GateRecord>,
39 ) -> Result<Self, ContractError> {
40 let record = Self {
41 contract: CONTRACT_VERSION,
42 producer: producer.into(),
43 digest: manifest.digest(),
44 manifest,
45 provenance,
46 gates,
47 };
48 record.validate()?;
49 Ok(record)
50 }
51
52 /// Everything an intake has to check before it may believe the document.
53 ///
54 /// Run on write as well as on read. A producer that emits a record it would
55 /// itself reject has a bug now, not at the far end of a transfer where the
56 /// error reads as corruption.
57 pub fn validate(&self) -> Result<(), ContractError> {
58 if self.contract != CONTRACT_VERSION {
59 return Err(ContractError::UnknownContractVersion(self.contract));
60 }
61 if self.producer.trim().is_empty() {
62 return Err(ContractError::MissingProvenance("producer"));
63 }
64 if self.digest != self.manifest.digest() {
65 return Err(ContractError::DigestMismatch {
66 claimed: self.digest.to_string(),
67 computed: self.manifest.digest().to_string(),
68 });
69 }
70 self.provenance.validate()?;
71 // A builder has no environment, so it cannot have observed one. This is
72 // the boundary refusing to be crossed by accident rather than a style
73 // rule: an environment-scoped gate from a build host is either
74 // mislabelled or measures a machine nothing will run on.
75 for g in &self.gates {
76 if let Scope::Environment { env } = &g.scope {
77 return Err(ContractError::EnvironmentEvidenceFromBuilder {
78 gate: g.gate.clone(),
79 env: env.clone(),
80 });
81 }
82 }
83 Ok(())
84 }
85
86 /// True iff every gate the builder ran passed. Not a decision: whether the
87 /// artifact advances is the evaluator's call, and it will have gates of its
88 /// own that this record cannot speak to.
89 pub fn all_gates_passed(&self) -> bool {
90 self.gates.iter().all(|g| g.verdict.is_passed())
91 }
92
93 pub fn to_json(&self) -> String {
94 // Pretty, with a trailing newline: these land on disk next to a bundle
95 // and get read by a person before they are ever read by a program.
96 let mut s = serde_json::to_string_pretty(self).unwrap_or_default();
97 s.push('\n');
98 s
99 }
100
101 pub fn parse(json: &str) -> Result<Self, ContractError> {
102 let record: Self = serde_json::from_str(json).map_err(ContractError::Malformed)?;
103 record.validate()?;
104 Ok(record)
105 }
106 }
107
108 /// What a later producer adds: I did not build this, and here is what I proved
109 /// about it.
110 ///
111 /// Separate from [`ArtifactRecord`] so that appending evidence never rewrites
112 /// the handover. Each producer owns one document; nobody edits anybody else's.
113 /// `subject` is the only link, which is what makes the binding between evidence
114 /// and bytes structural rather than a discipline each system has to remember.
115 #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
116 pub struct EvidenceRecord {
117 pub contract: u32,
118 /// Which system produced this (`sando`).
119 pub producer: String,
120 /// The bundle this evidence is about.
121 pub subject: BundleDigest,
122 pub gates: Vec<GateRecord>,
123 }
124
125 impl EvidenceRecord {
126 pub fn new(
127 producer: impl Into<String>,
128 subject: BundleDigest,
129 gates: Vec<GateRecord>,
130 ) -> Result<Self, ContractError> {
131 let record = Self {
132 contract: CONTRACT_VERSION,
133 producer: producer.into(),
134 subject,
135 gates,
136 };
137 record.validate()?;
138 Ok(record)
139 }
140
141 pub fn validate(&self) -> Result<(), ContractError> {
142 if self.contract != CONTRACT_VERSION {
143 return Err(ContractError::UnknownContractVersion(self.contract));
144 }
145 if self.producer.trim().is_empty() {
146 return Err(ContractError::MissingProvenance("producer"));
147 }
148 Ok(())
149 }
150
151 pub fn to_json(&self) -> String {
152 let mut s = serde_json::to_string_pretty(self).unwrap_or_default();
153 s.push('\n');
154 s
155 }
156
157 pub fn parse(json: &str) -> Result<Self, ContractError> {
158 let record: Self = serde_json::from_str(json).map_err(ContractError::Malformed)?;
159 record.validate()?;
160 Ok(record)
161 }
162 }
163
164 #[cfg(test)]
165 mod tests {
166 use super::*;
167 use crate::Verdict;
168 use chrono::{DateTime, Utc};
169
170 fn at() -> DateTime<Utc> {
171 DateTime::<Utc>::from_timestamp(1_754_000_000, 0).unwrap()
172 }
173
174 fn manifest() -> Manifest {
175 Manifest::new([
176 ("GoingsOn.AppImage", "1".repeat(64)),
177 ("GoingsOn.AppImage.minisig", "2".repeat(64)),
178 ])
179 .unwrap()
180 }
181
182 fn provenance() -> Provenance {
183 Provenance {
184 app: "goingson".into(),
185 version: "0.4.1".into(),
186 tag: "v0.4.1".into(),
187 git_sha: "a".repeat(40),
188 target: "linux/x86_64".into(),
189 build_host: "fw13".into(),
190 toolchain: "rustc 1.97.0".into(),
191 built_at: at(),
192 }
193 }
194
195 fn gate(name: &str, verdict: Verdict) -> GateRecord {
196 GateRecord::new(name, Scope::Artifact, verdict, "ok", at())
197 }
198
199 fn record() -> ArtifactRecord {
200 ArtifactRecord::new(
201 "bento",
202 manifest(),
203 provenance(),
204 vec![
205 gate("clippy", Verdict::Passed),
206 gate("cargo_test", Verdict::Passed),
207 ],
208 )
209 .unwrap()
210 }
211
212 #[test]
213 fn the_digest_is_the_manifests_and_the_record_round_trips() {
214 let r = record();
215 assert_eq!(r.digest, manifest().digest());
216 assert!(r.all_gates_passed());
217 let back = ArtifactRecord::parse(&r.to_json()).unwrap();
218 assert_eq!(r, back);
219 }
220
221 #[test]
222 fn a_record_whose_digest_does_not_match_its_manifest_is_refused() {
223 // The whole point of the handover: a document claiming to describe
224 // bytes it does not describe must not survive a read.
225 let mut r = record();
226 r.digest = BundleDigest::parse(&"f".repeat(64)).unwrap();
227 assert!(matches!(
228 r.validate().unwrap_err(),
229 ContractError::DigestMismatch { .. }
230 ));
231 assert!(ArtifactRecord::parse(&serde_json::to_string(&r).unwrap()).is_err());
232 }
233
234 #[test]
235 fn a_builder_cannot_hand_over_environment_evidence() {
236 let g = GateRecord::new(
237 "boot_smoke",
238 Scope::Environment {
239 env: "tier:a".into(),
240 },
241 Verdict::Passed,
242 "served /health",
243 at(),
244 );
245 let err = ArtifactRecord::new("bento", manifest(), provenance(), vec![g]).unwrap_err();
246 assert!(
247 matches!(err, ContractError::EnvironmentEvidenceFromBuilder { gate, env }
248 if gate == "boot_smoke" && env == "tier:a")
249 );
250 }
251
252 #[test]
253 fn a_failing_gate_does_not_invalidate_the_record() {
254 // A record of a failed build is a legitimate document. Refusing to emit
255 // one would leave the only evidence of the failure in a log.
256 let r = ArtifactRecord::new(
257 "bento",
258 manifest(),
259 provenance(),
260 vec![gate("cargo_test", Verdict::Failed)],
261 )
262 .unwrap();
263 assert!(!r.all_gates_passed());
264 }
265
266 #[test]
267 fn a_future_contract_version_is_refused_rather_than_guessed_at() {
268 let mut r = record();
269 r.contract = CONTRACT_VERSION + 1;
270 assert!(matches!(
271 r.validate().unwrap_err(),
272 ContractError::UnknownContractVersion(_)
273 ));
274 }
275
276 #[test]
277 fn evidence_names_the_bundle_it_is_about_and_may_be_environment_scoped() {
278 let e = EvidenceRecord::new(
279 "sando",
280 manifest().digest(),
281 vec![GateRecord::new(
282 "burn_in",
283 Scope::Environment {
284 env: "tier:a".into(),
285 },
286 Verdict::Blocked,
287 "12 hours remaining of 48",
288 at(),
289 )],
290 )
291 .unwrap();
292 let back = EvidenceRecord::parse(&e.to_json()).unwrap();
293 assert_eq!(back.subject, manifest().digest());
294 assert_eq!(back, e);
295 }
296
297 #[test]
298 fn appending_evidence_does_not_touch_the_handover() {
299 // Two documents, one subject. The evaluator's write cannot change the
300 // builder's document, and neither can change the digest.
301 let handover = record();
302 let _ = EvidenceRecord::new("sando", handover.digest.clone(), vec![]).unwrap();
303 assert_eq!(handover, record());
304 assert_eq!(handover.digest, manifest().digest());
305 }
306 }
307