in_toto/models/
metadata.rs

1//! in-toto metadata.
2//! # Metadata & MetadataWrapper
3//! Metadata is the top level abstract for both layout metadata and link
4//! metadata. Metadata it is devided into two types
5//!
6//! * enum `MetadataWrapper` is used to do serialize, deserialize and
7//!   other object unsafe operations.
8//! * trait `Metadata` is used to work for trait object.
9//!
10//! The reason please refer to issue <https://github.com/in-toto/in-toto-rs/issues/33>
11//!
12//! # Metablock
13//! Metablock is the container for link metadata and layout metadata.
14//! Its serialized outcome can work as the content of a link file
15//! or a layout file. It provides `MetablockBuilder` for create
16//! an instance of Metablock, and methods to verify signatures,
17//! create signatures.
18
19use log::{debug, warn};
20use serde::{Deserialize, Serialize};
21use std::collections::HashMap;
22use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
23use std::str;
24use strum::IntoEnumIterator;
25use strum_macros::EnumIter;
26
27use crate::crypto::{KeyId, PrivateKey, PublicKey, Signature};
28use crate::error::Error;
29use crate::interchange::{DataInterchange, Json};
30use crate::Result;
31
32use super::{LayoutMetadata, LinkMetadata};
33
34pub const FILENAME_FORMAT: &str = "{step_name}.{keyid:.8}.link";
35
36#[derive(
37    Debug, Serialize, Deserialize, Hash, PartialEq, Eq, EnumIter, Clone, Copy,
38)]
39pub enum MetadataType {
40    Layout,
41    Link,
42}
43
44impl Display for MetadataType {
45    fn fmt(&self, fmt: &mut Formatter) -> FmtResult {
46        match self {
47            MetadataType::Layout => fmt.write_str("layout")?,
48            MetadataType::Link => fmt.write_str("link")?,
49        }
50        Ok(())
51    }
52}
53
54#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
55#[serde(untagged)]
56pub enum MetadataWrapper {
57    Layout(LayoutMetadata),
58    Link(LinkMetadata),
59}
60
61impl MetadataWrapper {
62    /// Convert from enum `MetadataWrapper` to trait `Metadata`
63    pub fn into_trait(self) -> Box<dyn Metadata> {
64        match self {
65            MetadataWrapper::Layout(layout_meta) => Box::new(layout_meta),
66            MetadataWrapper::Link(link_meta) => Box::new(link_meta),
67        }
68    }
69
70    /// Standard deserialize for MetadataWrapper by its metadata
71    pub fn from_bytes(
72        bytes: &[u8],
73        metadata_type: MetadataType,
74    ) -> Result<Self> {
75        match metadata_type {
76            MetadataType::Layout => serde_json::from_slice(bytes)
77                .map(Self::Layout)
78                .map_err(|e| e.into()),
79            MetadataType::Link => serde_json::from_slice(bytes)
80                .map(Self::Link)
81                .map_err(|e| e.into()),
82        }
83    }
84
85    /// Auto deserialize for MetadataWrapper by any possible metadata.
86    pub fn try_from_bytes(bytes: &[u8]) -> Result<Self> {
87        let mut metadata: Result<MetadataWrapper> =
88            Err(Error::Programming("no available bytes parser".to_string()));
89        for typ in MetadataType::iter() {
90            metadata = MetadataWrapper::from_bytes(bytes, typ);
91            if metadata.is_ok() {
92                break;
93            }
94        }
95        metadata
96    }
97
98    /// Standard serialize for MetadataWrapper by its metadata
99    pub fn to_bytes(&self) -> Result<Vec<u8>> {
100        Json::canonicalize(&Json::serialize(self)?)
101    }
102}
103
104/// trait for Metadata
105pub trait Metadata {
106    /// The version of Metadata
107    fn typ(&self) -> MetadataType;
108    /// Convert from trait `Metadata` to enum `MetadataWrapper`
109    fn into_enum(self: Box<Self>) -> MetadataWrapper;
110    /// Standard serialize for Metadata
111    fn to_bytes(&self) -> Result<Vec<u8>>;
112}
113
114/// All signed files (link and layout files) have the format.
115/// * `signatures`: A pubkey => signature map. signatures are for the metadata.
116/// * `metadata`: <ROLE> dictionary. Also known as signed metadata. e.g., link
117///   or layout.
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
119pub struct Metablock {
120    pub signatures: Vec<Signature>,
121    #[serde(rename = "signed")]
122    pub metadata: MetadataWrapper,
123}
124
125impl Metablock {
126    /// Create a new Metablock, using data of metadata. And the signatures are
127    /// generated by using private-keys to sign the metadata.
128    pub fn new(
129        metadata: MetadataWrapper,
130        private_keys: &[&PrivateKey],
131    ) -> Result<Self> {
132        let raw = metadata.to_bytes()?;
133        let metadata_string = String::from_utf8(raw)
134            .map_err(|e| {
135                Error::Encoding(format!(
136                    "Cannot convert metadata into a string: {}",
137                    e
138                ))
139            })?
140            .replace("\\n", "\n");
141
142        // sign and collect signatures
143        let mut signatures = Vec::new();
144        private_keys.iter().try_for_each(|key| -> Result<()> {
145            let sig = key.sign(metadata_string.as_bytes())?;
146            signatures.push(sig);
147            Ok(())
148        })?;
149
150        Ok(Self {
151            signatures,
152            metadata,
153        })
154    }
155
156    /// Verify this metadata.
157    /// Each signature in the Metablock signed by an authorized key
158    /// is a legal signature. Only legal the number signatures is
159    /// not less than `threshold`, will return the wrapped Metadata.
160    pub fn verify<'a, I>(
161        &self,
162        threshold: u32,
163        authorized_keys: I,
164    ) -> Result<MetadataWrapper>
165    where
166        I: IntoIterator<Item = &'a PublicKey>,
167    {
168        if self.signatures.is_empty() {
169            return Err(Error::VerificationFailure(
170                "The metadata was not signed with any authorized keys.".into(),
171            ));
172        }
173
174        if threshold < 1 {
175            return Err(Error::VerificationFailure(
176                "Threshold must be strictly greater than zero".into(),
177            ));
178        }
179
180        let authorized_keys = authorized_keys
181            .into_iter()
182            .map(|k| (k.key_id(), k))
183            .collect::<HashMap<&KeyId, &PublicKey>>();
184
185        let raw = self.metadata.to_bytes()?;
186        let metadata = String::from_utf8(raw)
187            .map_err(|e| {
188                Error::Encoding(format!(
189                    "Cannot convert metadata into a string: {}",
190                    e
191                ))
192            })?
193            .replace("\\n", "\n");
194        let mut signatures_needed = threshold;
195
196        // Create a key_id->signature map to deduplicate the key_ids.
197        let signatures = self
198            .signatures
199            .iter()
200            .map(|sig| (sig.key_id(), sig))
201            .collect::<HashMap<&KeyId, &Signature>>();
202
203        // check the signatures, if is signed by an authorized key,
204        // signatures_needed - 1
205
206        for (key_id, sig) in signatures {
207            match authorized_keys.get(key_id) {
208                Some(pub_key) => match pub_key.verify(metadata.as_bytes(), sig)
209                {
210                    Ok(()) => {
211                        debug!(
212                            "Good signature from key ID {:?}",
213                            pub_key.key_id()
214                        );
215                        signatures_needed -= 1;
216                    }
217                    Err(e) => {
218                        warn!(
219                            "Bad signature from key ID {:?}: {:?}",
220                            pub_key.key_id(),
221                            e
222                        );
223                    }
224                },
225                None => {
226                    warn!(
227                        "Key ID {:?} was not found in the set of authorized keys.",
228                        sig.key_id()
229                    );
230                }
231            }
232            if signatures_needed == 0 {
233                break;
234            }
235        }
236
237        if signatures_needed > 0 {
238            return Err(Error::VerificationFailure(format!(
239                "Signature threshold not met: {}/{}",
240                threshold - signatures_needed,
241                threshold
242            )));
243        }
244
245        Ok(self.metadata.clone())
246    }
247}
248
249/// A helper to build Metablock
250pub struct MetablockBuilder {
251    signatures: HashMap<KeyId, Signature>,
252    metadata: MetadataWrapper,
253}
254
255impl MetablockBuilder {
256    /// Create a new `MetablockBuilder` from a given `Metadata`.
257    pub fn from_metadata(metadata: Box<dyn Metadata>) -> Self {
258        Self {
259            signatures: HashMap::new(),
260            metadata: metadata.into_enum(),
261        }
262    }
263
264    /// Create a new `MetablockBuilder` from manually serialized metadata to be signed.
265    /// Returns an error if `metadata` cannot be parsed into Metadata.
266    pub fn from_raw_metadata(raw_metadata: &[u8]) -> Result<Self> {
267        let metadata = MetadataWrapper::try_from_bytes(raw_metadata)?;
268        Ok(Self {
269            signatures: HashMap::new(),
270            metadata,
271        })
272    }
273
274    /// Sign the metadata using the given `private_keys`, replacing any existing signatures with the
275    /// same `KeyId`.
276    pub fn sign(mut self, private_keys: &[&PrivateKey]) -> Result<Self> {
277        let mut signatures = HashMap::new();
278        let raw = self.metadata.to_bytes()?;
279        let metadata = String::from_utf8(raw)
280            .map_err(|e| {
281                Error::Encoding(format!(
282                    "Cannot convert metadata into a string: {}",
283                    e
284                ))
285            })?
286            .replace("\\n", "\n");
287
288        private_keys.iter().try_for_each(|key| -> Result<()> {
289            let sig = key.sign(metadata.as_bytes())?;
290            signatures.insert(sig.key_id().clone(), sig);
291            Ok(())
292        })?;
293
294        self.signatures = signatures;
295        Ok(self)
296    }
297
298    /// Construct a new `Metablock` using the included signatures, sorting the signatures by
299    /// `KeyId`.
300    pub fn build(self) -> Metablock {
301        let mut signatures = self.signatures.into_values().collect::<Vec<_>>();
302        signatures.sort_unstable_by(|a, b| a.key_id().cmp(b.key_id()));
303
304        Metablock {
305            signatures,
306            metadata: self.metadata,
307        }
308    }
309}
310
311#[cfg(test)]
312mod tests {
313    use std::{fs, str::FromStr};
314
315    use assert_json_diff::assert_json_eq;
316    use chrono::DateTime;
317    use serde_json::json;
318
319    use crate::{
320        crypto::{PrivateKey, PublicKey},
321        models::{
322            byproducts::ByProducts,
323            inspection::Inspection,
324            rule::{Artifact, ArtifactRule},
325            step::{Command, Step},
326            LayoutMetadataBuilder, LinkMetadataBuilder, Metablock,
327            VirtualTargetPath,
328        },
329    };
330
331    use super::MetablockBuilder;
332
333    const ALICE_PRIVATE_KEY: &'static [u8] =
334        include_bytes!("../../tests/ed25519/ed25519-1");
335    const ALICE_PUB_KEY: &'static [u8] =
336        include_bytes!("../../tests/ed25519/ed25519-1.pub");
337    const BOB_PUB_KEY: &'static [u8] =
338        include_bytes!("../../tests/rsa/rsa-4096.spki.der");
339    const OWNER_PRIVATE_KEY: &'static [u8] =
340        include_bytes!("../../tests/test_metadata/owner.der");
341
342    #[test]
343    fn deserialize_layout_metablock() {
344        let raw = fs::read("tests/test_metadata/demo.layout").unwrap();
345        assert!(serde_json::from_slice::<Metablock>(&raw).is_ok());
346    }
347
348    #[test]
349    fn deserialize_link_metablock() {
350        let raw = fs::read("tests/test_metadata/demo.link").unwrap();
351        assert!(serde_json::from_slice::<Metablock>(&raw).is_ok());
352    }
353
354    #[test]
355    fn serialize_layout_metablock() {
356        let alice_public_key = PublicKey::from_ed25519(ALICE_PUB_KEY).unwrap();
357        let bob_public_key = PublicKey::from_spki(
358            BOB_PUB_KEY,
359            crate::crypto::SignatureScheme::RsaSsaPssSha256,
360        )
361        .unwrap();
362        let owner_private_key =
363            PrivateKey::from_ed25519(OWNER_PRIVATE_KEY).unwrap();
364        let layout_metadata = Box::new(
365            LayoutMetadataBuilder::new()
366                .expires(DateTime::UNIX_EPOCH)
367                .add_key(alice_public_key.clone())
368                .add_key(bob_public_key.clone())
369                .add_step(
370                    Step::new("write-code")
371                        .threshold(1)
372                        .add_expected_product(ArtifactRule::Create(
373                            "foo.py".into(),
374                        ))
375                        .expected_command(Command::from_str("vi").unwrap())
376                        .add_key(alice_public_key.key_id().to_owned()),
377                )
378                .add_step(
379                    Step::new("package")
380                        .threshold(1)
381                        .add_expected_material(ArtifactRule::Match {
382                            pattern: "foo.py".into(),
383                            in_src: None,
384                            with: Artifact::Products,
385                            in_dst: None,
386                            from: "write-code".into(),
387                        })
388                        .add_expected_product(ArtifactRule::Create(
389                            "foo.tar.gz".into(),
390                        ))
391                        .expected_command(
392                            Command::from_str("tar zcvf foo.tar.gz foo.py")
393                                .unwrap(),
394                        )
395                        .add_key(bob_public_key.key_id().to_owned()),
396                )
397                .add_inspect(
398                    Inspection::new("inspect_tarball")
399                        .add_expected_material(ArtifactRule::Match {
400                            pattern: "foo.tar.gz".into(),
401                            in_src: None,
402                            with: Artifact::Products,
403                            in_dst: None,
404                            from: "package".into(),
405                        })
406                        .add_expected_product(ArtifactRule::Match {
407                            pattern: "foo.py".into(),
408                            in_src: None,
409                            with: Artifact::Products,
410                            in_dst: None,
411                            from: "write-code".into(),
412                        })
413                        .run(
414                            Command::from_str("inspect_tarball.sh foo.tar.gz")
415                                .unwrap(),
416                        ),
417                )
418                .build()
419                .unwrap(),
420        );
421
422        let private_keys = vec![&owner_private_key];
423        let metablock = MetablockBuilder::from_metadata(layout_metadata)
424            .sign(&private_keys)
425            .unwrap()
426            .build();
427
428        let serialized = serde_json::to_value(&metablock).unwrap();
429        let expected = json!({
430            "signed": {
431                "_type": "layout",
432                "expires": "1970-01-01T00:00:00Z",
433                "readme": "",
434                "keys": {
435                    "59d12f31ee173dbb3359769414e73c120f219af551baefb70aa69414dfba4aaf": {
436                        "keyid": "59d12f31ee173dbb3359769414e73c120f219af551baefb70aa69414dfba4aaf",
437                        "keytype": "rsa",
438                        "scheme": "rsassa-pss-sha256",
439                        "keyid_hash_algorithms": [
440                            "sha256",
441                            "sha512"
442                        ],
443                        "keyval": {
444                            "private": "",
445                            "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-----"
446                        }
447                    },
448                    "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554": {
449                        "keyid": "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554",
450                        "keytype": "ed25519",
451                        "scheme": "ed25519",
452                        "keyval": {
453                            "private": "",
454                            "public": "eb8ac26b5c9ef0279e3be3e82262a93bce16fe58ee422500d38caf461c65a3b6"
455                        }
456                    }
457                },
458                "steps": [
459                    {
460                      "_type": "step",
461                      "name": "write-code",
462                      "threshold": 1,
463                      "expected_materials": [ ],
464                      "expected_products": [
465                          ["CREATE", "foo.py"]
466                      ],
467                      "pubkeys": [
468                          "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554"
469                      ],
470                      "expected_command": ["vi"]
471                    },
472                    {
473                      "_type": "step",
474                      "name": "package",
475                      "threshold": 1,
476                      "expected_materials": [
477                          ["MATCH", "foo.py", "WITH", "PRODUCTS", "FROM", "write-code"]
478                      ],
479                      "expected_products": [
480                          ["CREATE", "foo.tar.gz"]
481                      ],
482                      "pubkeys": [
483                          "59d12f31ee173dbb3359769414e73c120f219af551baefb70aa69414dfba4aaf"
484                      ],
485                      "expected_command": ["tar", "zcvf", "foo.tar.gz", "foo.py"]
486                    }],
487                  "inspect": [
488                    {
489                      "_type": "inspection",
490                      "name": "inspect_tarball",
491                      "expected_materials": [
492                          ["MATCH", "foo.tar.gz", "WITH", "PRODUCTS", "FROM", "package"]
493                      ],
494                      "expected_products": [
495                          ["MATCH", "foo.py", "WITH", "PRODUCTS", "FROM", "write-code"]
496                      ],
497                      "run": ["inspect_tarball.sh", "foo.tar.gz"]
498                    }
499                  ],
500                  "readme": ""
501                },
502            "signatures": [{
503                "keyid" : "64786e5921b589af1ca1bf5767087bf201806a9b3ce2e6856c903682132bd1dd",
504                "sig": "61b2551e3febfa1f110cd9f087243908d88d29fb639b83e7978f9e3bda109cb21452134534298c64825c85684700390fcd0a0f03ee468905405ec58f88becb06"
505            }]
506        });
507        assert_json_eq!(expected, serialized);
508    }
509
510    #[test]
511    fn serialize_link_metablock() {
512        let link_metadata = LinkMetadataBuilder::new()
513            .name("".into())
514            .add_product(
515                VirtualTargetPath::new("tests/test_link/foo.tar.gz".into())
516                    .unwrap(),
517            )
518            .byproducts(
519                ByProducts::new()
520                    .set_return_value(0)
521                    .set_stderr("a foo.py\n".into())
522                    .set_stdout("".into()),
523            )
524            .command(Command::from("tar zcvf foo.tar.gz foo.py"))
525            .build()
526            .unwrap();
527        let alice_public_key =
528            PrivateKey::from_ed25519(ALICE_PRIVATE_KEY).unwrap();
529        let private_keys = vec![&alice_public_key];
530        let metablock =
531            MetablockBuilder::from_metadata(Box::new(link_metadata))
532                .sign(&private_keys)
533                .unwrap()
534                .build();
535        let serialized = serde_json::to_value(&metablock).unwrap();
536        let expected = json!({
537            "signed" : {
538                "_type": "link",
539                "name": "",
540                "materials": {},
541                "products": {
542                    "tests/test_link/foo.tar.gz": {
543                        "sha256": "52947cb78b91ad01fe81cd6aef42d1f6817e92b9e6936c1e5aabb7c98514f355"
544                    }
545                },
546                "byproducts": {
547                    "return-value": 0,
548                    "stderr": "a foo.py\n",
549                    "stdout": ""
550                },
551                "command": ["tar", "zcvf", "foo.tar.gz", "foo.py"],
552                "environment": null
553            },
554            "signatures" : [{
555                "keyid" : "e0294a3f17cc8563c3ed5fceb3bd8d3f6bfeeaca499b5c9572729ae015566554",
556                "sig": "62918f5f84fca149c15fcbc247a831e0360d33f0d9c8a89e6f623a011a8b807e2b0ef816a37356d966e9ad446ec234efb2b3bb4b04f338c0560d9cdfa1dcba0a"
557            }]
558        });
559        assert_eq!(expected, serialized);
560    }
561
562    #[test]
563    fn verify_signatures_of_metablock() {
564        let link_metadata = LinkMetadataBuilder::new()
565            .name("".into())
566            .add_product(
567                VirtualTargetPath::new("tests/test_link/foo.tar.gz".into())
568                    .unwrap(),
569            )
570            .byproducts(
571                ByProducts::new()
572                    .set_return_value(0)
573                    .set_stderr("a foo.py\n".into())
574                    .set_stdout("".into()),
575            )
576            .command(Command::from("tar zcvf foo.tar.gz foo.py"))
577            .build()
578            .unwrap();
579        let alice_public_key =
580            PrivateKey::from_ed25519(ALICE_PRIVATE_KEY).unwrap();
581        let private_keys = vec![&alice_public_key];
582        let metablock =
583            MetablockBuilder::from_metadata(Box::new(link_metadata))
584                .sign(&private_keys)
585                .unwrap()
586                .build();
587
588        let public_key = PublicKey::from_ed25519(ALICE_PUB_KEY).unwrap();
589        let authorized_keys = vec![&public_key];
590        assert!(metablock.verify(1, authorized_keys).is_ok());
591    }
592}