1use 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 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 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 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 pub fn to_bytes(&self) -> Result<Vec<u8>> {
100 Json::canonicalize(&Json::serialize(self)?)
101 }
102}
103
104pub trait Metadata {
106 fn typ(&self) -> MetadataType;
108 fn into_enum(self: Box<Self>) -> MetadataWrapper;
110 fn to_bytes(&self) -> Result<Vec<u8>>;
112}
113
114#[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 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 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 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 let signatures = self
198 .signatures
199 .iter()
200 .map(|sig| (sig.key_id(), sig))
201 .collect::<HashMap<&KeyId, &Signature>>();
202
203 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
249pub struct MetablockBuilder {
251 signatures: HashMap<KeyId, Signature>,
252 metadata: MetadataWrapper,
253}
254
255impl MetablockBuilder {
256 pub fn from_metadata(metadata: Box<dyn Metadata>) -> Self {
258 Self {
259 signatures: HashMap::new(),
260 metadata: metadata.into_enum(),
261 }
262 }
263
264 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 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 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}