1use std::collections::BTreeMap;
4
5use chrono::prelude::*;
6use chrono::{DateTime, Utc};
7use log::warn;
8use serde::{Deserialize, Serialize};
9
10use crate::crypto::{KeyId, PublicKey};
11use crate::{Error, Result};
12
13use self::{inspection::Inspection, step::Step};
14
15pub mod inspection;
16mod metadata;
17pub mod rule;
18pub mod step;
19pub mod supply_chain_item;
20
21pub use metadata::{LayoutMetadata, LayoutMetadataBuilder};
22
23#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
24pub struct Layout {
25 #[serde(rename = "_type")]
26 typ: String,
27 expires: String,
28 readme: String,
29 keys: BTreeMap<KeyId, PublicKey>,
30 steps: Vec<Step>,
31 inspect: Vec<Inspection>,
32}
33
34impl Layout {
35 pub fn from(meta: &LayoutMetadata) -> Result<Self> {
36 Ok(Layout {
37 typ: String::from("layout"),
38 expires: format_datetime(&meta.expires),
39 readme: meta.readme.to_string(),
40 keys: meta
41 .keys
42 .iter()
43 .map(|(id, key)| (id.clone(), key.clone()))
44 .collect(),
45 steps: meta.steps.clone(),
46 inspect: meta.inspect.clone(),
47 })
48 }
49
50 pub fn try_into(self) -> Result<LayoutMetadata> {
51 let keys_with_correct_key_id = self
54 .keys
55 .into_iter()
56 .filter(|(key_id, pkey)| {
57 if key_id == pkey.key_id() {
58 true
59 } else {
60 warn!("Malformed key of ID {:?}", key_id);
61 false
62 }
63 })
64 .collect();
65
66 Ok(LayoutMetadata::new(
67 parse_datetime(&self.expires)?,
68 self.readme,
69 keys_with_correct_key_id,
70 self.steps,
71 self.inspect,
72 ))
73 }
74}
75
76fn parse_datetime(ts: &str) -> Result<DateTime<Utc>> {
77 let dt = DateTime::parse_from_rfc3339(ts).map_err(|e| {
78 Error::Encoding(format!("Can't parse DateTime: {:?}", e))
79 })?;
80 Ok(dt.with_timezone(&Utc))
81}
82
83fn format_datetime(ts: &DateTime<Utc>) -> String {
84 ts.to_rfc3339_opts(SecondsFormat::Secs, true)
85}
86
87#[cfg(test)]
88mod test {
89 use assert_json_diff::assert_json_eq;
90 use chrono::DateTime;
91 use serde_json::json;
92
93 use crate::{crypto::PublicKey, models::layout::format_datetime};
94
95 use super::{
96 inspection::Inspection,
97 parse_datetime,
98 rule::{Artifact, ArtifactRule},
99 step::Step,
100 Layout, LayoutMetadataBuilder,
101 };
102
103 const ALICE_PUB_KEY: &'static [u8] =
104 include_bytes!("../../../tests/ed25519/ed25519-1.pub");
105 const BOB_PUB_KEY: &'static [u8] =
106 include_bytes!("../../../tests/rsa/rsa-4096.spki.der");
107
108 #[test]
109 fn parse_datetime_test() {
110 let time_str = "1970-01-01T00:00:00Z".to_string();
111 let parsed_dt = parse_datetime(&time_str[..]).unwrap();
112 let dt = DateTime::UNIX_EPOCH;
113 assert_eq!(parsed_dt, dt);
114 }
115
116 #[test]
117 fn format_datetime_test() {
118 let dt = DateTime::UNIX_EPOCH;
119 let generated_dt_str = format_datetime(&dt);
120 let dt_str = "1970-01-01T00:00:00Z".to_string();
121 assert_eq!(dt_str, generated_dt_str);
122 }
123
124 fn get_example_layout_metadata() -> Layout {
125 let alice_key = PublicKey::from_ed25519(ALICE_PUB_KEY).unwrap();
126 let bob_key = PublicKey::from_spki(
127 BOB_PUB_KEY,
128 crate::crypto::SignatureScheme::RsaSsaPssSha256,
129 )
130 .unwrap();
131 let metadata = LayoutMetadataBuilder::new()
132 .expires(DateTime::UNIX_EPOCH)
133 .add_key(alice_key.clone())
134 .add_key(bob_key.clone())
135 .add_step(
136 Step::new("write-code")
137 .threshold(1)
138 .add_expected_product(ArtifactRule::Create("foo.py".into()))
139 .add_key(alice_key.key_id().to_owned())
140 .expected_command("vi".into()),
141 )
142 .add_step(
143 Step::new("package")
144 .threshold(1)
145 .add_expected_material(ArtifactRule::Match {
146 pattern: "foo.py".into(),
147 in_src: None,
148 with: Artifact::Products,
149 in_dst: None,
150 from: "write-code".into(),
151 })
152 .add_expected_product(ArtifactRule::Create(
153 "foo.tar.gz".into(),
154 ))
155 .add_key(bob_key.key_id().to_owned())
156 .expected_command("tar zcvf foo.tar.gz foo.py".into()),
157 )
158 .add_inspect(
159 Inspection::new("inspect_tarball")
160 .add_expected_material(ArtifactRule::Match {
161 pattern: "foo.tar.gz".into(),
162 in_src: None,
163 with: Artifact::Products,
164 in_dst: None,
165 from: "package".into(),
166 })
167 .add_expected_product(ArtifactRule::Match {
168 pattern: "foo.py".into(),
169 in_src: None,
170 with: Artifact::Products,
171 in_dst: None,
172 from: "write-code".into(),
173 })
174 .run("inspect_tarball.sh foo.tar.gz".into()),
175 )
176 .readme("".into())
177 .build()
178 .unwrap();
179 Layout::from(&metadata).unwrap()
180 }
181
182 #[test]
183 fn serialize_layout() {
184 let layout = get_example_layout_metadata();
185 let json = json!({
186 "_type": "layout",
187 "expires": "1970-01-01T00:00:00Z",
188 "readme": "",
189 "keys": {
190 "59d12f31ee173dbb3359769414e73c120f219af551baefb70aa69414dfba4aaf": {
191 "keyid": "59d12f31ee173dbb3359769414e73c120f219af551baefb70aa69414dfba4aaf",
192 "keytype": "rsa",
193 "scheme": "rsassa-pss-sha256",
194 "keyid_hash_algorithms": [
195 "sha256",
196 "sha512"
197 ],
198 "keyval": {
199 "private": "",
200 "public": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA91+6CJmBzrb6ODSXPvVK\nh9IVvDkD63d5/wHawj1ZB22Y0R7A7b8lRl7IqJJ3TcZO8W2zFfeRuPFlghQs+O7h\nA6XiRr4mlD1dLItk+p93E0vgY+/Jj4I09LObgA2ncGw/bUlYt3fB5tbmnojQyhrQ\nwUQvBxOqI3nSglg02mCdQRWpPzerGxItOIQkmU2TsqTg7TZ8lnSUbAsFuMebnA2d\nJ2hzeou7ZGsyCJj/6O0ORVF37nLZiOFF8EskKVpUJuoLWopEA2c09YDgFWHEPTIo\nGNWB2l/qyX7HTk1wf+WK/Wnn3nerzdEhY9dH+U0uH7tOBBVCyEKxUqXDGpzuLSxO\nGBpJXa3TTqLHJWIOzhIjp5J3rV93aeSqemU38KjguZzdwOMO5lRsFco5gaFS9aNL\nLXtLd4ZgXaxB3vYqFDhvZCx4IKrsYEc/Nr8ubLwyQ8WHeS7v8FpIT7H9AVNDo9BM\nZpnmdTc5Lxi15/TulmswIIgjDmmIqujUqyHN27u7l6bZJlcn8lQdYMm4eJr2o+Jt\ndloTwm7Cv/gKkhZ5tdO5c/219UYBnKaGF8No1feEHirm5mdvwpngCxdFMZMbfmUA\nfzPeVPkXE+LR0lsLGnMlXKG5vKFcQpCXW9iwJ4pZl7j12wLwiWyLDQtsIxiG6Sds\nALPkWf0mnfBaVj/Q4FNkJBECAwEAAQ==\n-----END PUBLIC KEY-----"
201 }
202 },
203 "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554": {
204 "keyid": "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554",
205 "keytype": "ed25519",
206 "scheme": "ed25519",
207 "keyval": {
208 "private": "",
209 "public": "eb8ac26b5c9ef0279e3be3e82262a93bce16fe58ee422500d38caf461c65a3b6"
210 }
211 }
212 },
213 "steps": [
214 {
215 "_type": "step",
216 "name": "write-code",
217 "threshold": 1,
218 "expected_materials": [ ],
219 "expected_products": [
220 ["CREATE", "foo.py"]
221 ],
222 "pubkeys": [
223 "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554"
224 ],
225 "expected_command": ["vi"]
226 },
227 {
228 "_type": "step",
229 "name": "package",
230 "threshold": 1,
231 "expected_materials": [
232 ["MATCH", "foo.py", "WITH", "PRODUCTS", "FROM", "write-code"]
233 ],
234 "expected_products": [
235 ["CREATE", "foo.tar.gz"]
236 ],
237 "pubkeys": [
238 "59d12f31ee173dbb3359769414e73c120f219af551baefb70aa69414dfba4aaf"
239 ],
240 "expected_command": ["tar", "zcvf", "foo.tar.gz", "foo.py"]
241 }],
242 "inspect": [
243 {
244 "_type": "inspection",
245 "name": "inspect_tarball",
246 "expected_materials": [
247 ["MATCH", "foo.tar.gz", "WITH", "PRODUCTS", "FROM", "package"]
248 ],
249 "expected_products": [
250 ["MATCH", "foo.py", "WITH", "PRODUCTS", "FROM", "write-code"]
251 ],
252 "run": ["inspect_tarball.sh", "foo.tar.gz"]
253 }
254 ]
255 });
256
257 let json_serialize = serde_json::to_value(&layout).unwrap();
258 assert_json_eq!(json, json_serialize);
259 }
260
261 #[test]
262 fn deserialize_layout() {
263 let json = r#"{
264 "_type": "layout",
265 "expires": "1970-01-01T00:00:00Z",
266 "readme": "",
267 "keys": {
268 "59d12f31ee173dbb3359769414e73c120f219af551baefb70aa69414dfba4aaf": {
269 "keytype": "rsa",
270 "scheme": "rsassa-pss-sha256",
271 "keyid_hash_algorithms": [
272 "sha256",
273 "sha512"
274 ],
275 "keyval": {
276 "public": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA91+6CJmBzrb6ODSXPvVK\nh9IVvDkD63d5/wHawj1ZB22Y0R7A7b8lRl7IqJJ3TcZO8W2zFfeRuPFlghQs+O7h\nA6XiRr4mlD1dLItk+p93E0vgY+/Jj4I09LObgA2ncGw/bUlYt3fB5tbmnojQyhrQ\nwUQvBxOqI3nSglg02mCdQRWpPzerGxItOIQkmU2TsqTg7TZ8lnSUbAsFuMebnA2d\nJ2hzeou7ZGsyCJj/6O0ORVF37nLZiOFF8EskKVpUJuoLWopEA2c09YDgFWHEPTIo\nGNWB2l/qyX7HTk1wf+WK/Wnn3nerzdEhY9dH+U0uH7tOBBVCyEKxUqXDGpzuLSxO\nGBpJXa3TTqLHJWIOzhIjp5J3rV93aeSqemU38KjguZzdwOMO5lRsFco5gaFS9aNL\nLXtLd4ZgXaxB3vYqFDhvZCx4IKrsYEc/Nr8ubLwyQ8WHeS7v8FpIT7H9AVNDo9BM\nZpnmdTc5Lxi15/TulmswIIgjDmmIqujUqyHN27u7l6bZJlcn8lQdYMm4eJr2o+Jt\ndloTwm7Cv/gKkhZ5tdO5c/219UYBnKaGF8No1feEHirm5mdvwpngCxdFMZMbfmUA\nfzPeVPkXE+LR0lsLGnMlXKG5vKFcQpCXW9iwJ4pZl7j12wLwiWyLDQtsIxiG6Sds\nALPkWf0mnfBaVj/Q4FNkJBECAwEAAQ==\n-----END PUBLIC KEY-----"
277 }
278 },
279 "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554": {
280 "keytype": "ed25519",
281 "scheme": "ed25519",
282 "keyval": {
283 "public": "eb8ac26b5c9ef0279e3be3e82262a93bce16fe58ee422500d38caf461c65a3b6"
284 }
285 }
286 },
287 "steps": [
288 {
289 "_type": "step",
290 "name": "write-code",
291 "threshold": 1,
292 "expected_materials": [],
293 "expected_products": [
294 [
295 "CREATE",
296 "foo.py"
297 ]
298 ],
299 "pubkeys": [
300 "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554"
301 ],
302 "expected_command": [
303 "vi"
304 ]
305 },
306 {
307 "_type": "step",
308 "name": "package",
309 "threshold": 1,
310 "expected_materials": [
311 [
312 "MATCH",
313 "foo.py",
314 "WITH",
315 "PRODUCTS",
316 "FROM",
317 "write-code"
318 ]
319 ],
320 "expected_products": [
321 [
322 "CREATE",
323 "foo.tar.gz"
324 ]
325 ],
326 "pubkeys": [
327 "59d12f31ee173dbb3359769414e73c120f219af551baefb70aa69414dfba4aaf"
328 ],
329 "expected_command": [
330 "tar",
331 "zcvf",
332 "foo.tar.gz",
333 "foo.py"
334 ]
335 }
336 ],
337 "inspect": [
338 {
339 "_type": "inspection",
340 "name": "inspect_tarball",
341 "expected_materials": [
342 [
343 "MATCH",
344 "foo.tar.gz",
345 "WITH",
346 "PRODUCTS",
347 "FROM",
348 "package"
349 ]
350 ],
351 "expected_products": [
352 [
353 "MATCH",
354 "foo.py",
355 "WITH",
356 "PRODUCTS",
357 "FROM",
358 "write-code"
359 ]
360 ],
361 "run": [
362 "inspect_tarball.sh",
363 "foo.tar.gz"
364 ]
365 }
366 ]
367 }"#;
368
369 let layout = get_example_layout_metadata();
370 let layout_parse: Layout = serde_json::from_str(json).unwrap();
371 assert_eq!(layout, layout_parse);
372 }
373}