in_toto/models/layout/
mod.rs

1//! in-toto layout: used by the project owner to generate a desired supply chain layout file.
2
3use 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        // Ignore all keys with incorrect key IDs.
52        // If a malformed key is used, there will be a warning
53        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}