1mod api;
5mod bindle_spec;
6mod condition;
7mod group;
8mod label;
9mod parcel;
10mod sealed;
11pub mod signature;
12pub mod verification;
13
14#[cfg(feature = "client")]
15#[doc(inline)]
16pub(crate) use api::DeviceAuthorizationExtraFields;
17#[cfg(any(feature = "client", feature = "server"))]
18#[doc(inline)]
19pub(crate) use api::LoginParams;
20#[doc(inline)]
21pub use api::{
22 ErrorResponse, HealthResponse, InvoiceCreateResponse, KeyOptions, MissingParcelsResponse,
23 QueryOptions,
24};
25#[doc(inline)]
26pub use bindle_spec::BindleSpec;
27#[doc(inline)]
28pub use condition::Condition;
29#[doc(inline)]
30pub use group::Group;
31#[doc(inline)]
32pub use label::Label;
33#[doc(inline)]
34pub use parcel::Parcel;
35#[doc(inline)]
36pub use signature::{SecretKeyEntry, Signature, SignatureError, SignatureRole};
37#[doc(inline)]
38pub use verification::VerificationStrategy;
39
40use ed25519_dalek::{Signature as EdSignature, Signer};
41use semver::{Version, VersionReq};
42use serde::{Deserialize, Serialize};
43use tracing::info;
44
45use std::borrow::{Borrow, BorrowMut};
46use std::collections::BTreeMap;
47use std::fmt::Debug;
48use std::time::{SystemTime, UNIX_EPOCH};
49
50use self::verification::Verified;
51use crate::BINDLE_VERSION_1;
52
53pub type FeatureMap = BTreeMap<String, BTreeMap<String, String>>;
55
56pub type AnnotationMap = BTreeMap<String, String>;
58
59pub trait Signed: sealed::Sealed {
62 fn signed(self) -> Invoice;
64}
65
66#[derive(Serialize, Deserialize, Debug, Clone)]
75#[serde(deny_unknown_fields, rename_all = "camelCase")]
76pub struct Invoice {
77 pub bindle_version: String,
78 pub yanked: Option<bool>,
79 pub yanked_signature: Option<Vec<Signature>>,
80 pub bindle: BindleSpec,
81 pub annotations: Option<AnnotationMap>,
82 pub parcel: Option<Vec<Parcel>>,
83 pub group: Option<Vec<Group>>,
84 pub signature: Option<Vec<Signature>>,
85}
86
87impl Invoice {
88 pub fn new(spec: BindleSpec) -> Self {
92 Invoice {
93 bindle_version: BINDLE_VERSION_1.to_owned(),
94 bindle: spec,
95 parcel: None,
96 yanked: None,
97 yanked_signature: None,
98 annotations: None,
99 signature: None,
100 group: None,
101 }
102 }
103
104 pub fn name(&self) -> String {
109 format!("{}/{}", self.bindle.id.name(), self.bindle.id.version())
110 }
111
112 pub fn canonical_name(&self) -> String {
121 self.bindle.id.sha()
122 }
123
124 pub(crate) fn version_in_range(&self, requirement: &str) -> bool {
137 version_compare(self.bindle.id.version(), requirement)
138 }
139
140 pub fn has_group(&self, name: &str) -> bool {
142 let empty = Vec::with_capacity(0);
143 self.group
144 .as_ref()
145 .unwrap_or(&empty)
146 .iter()
147 .any(|g| g.name == name)
148 }
149
150 pub fn group_members(&self, name: &str) -> Vec<Parcel> {
152 if !self.has_group(name) {
154 info!(name, "no such group");
155 return vec![];
156 }
157
158 let zero_vec = Vec::with_capacity(0);
159 self.parcel
160 .as_ref()
161 .unwrap_or(&zero_vec)
162 .iter()
163 .filter(|p| p.member_of(name))
164 .cloned()
165 .collect()
166 }
167
168 fn cleartext(&self, by: &str, role: &SignatureRole) -> String {
169 let mut buf = vec![
170 by.to_owned(),
171 self.bindle.id.name().to_owned(),
172 self.bindle.id.version_string(),
173 role.to_string(),
174 '~'.to_string(),
175 ];
176
177 if let Some(list) = self.parcel.as_ref() {
179 list.iter().for_each(|p| {
180 buf.push(p.label.sha256.clone());
181 })
182 }
183
184 buf.join("\n")
185 }
186
187 pub fn sign(
198 &mut self,
199 signer_role: SignatureRole,
200 keyfile: &SecretKeyEntry,
201 ) -> Result<(), SignatureError> {
202 let signer_name = keyfile.label.clone();
203 let key = keyfile.key()?;
204 let encoded_key = base64::encode(key.public.to_bytes());
207 if let Some(sigs) = self.signature.as_ref() {
208 for s in sigs {
209 if s.key == encoded_key {
210 return Err(SignatureError::DuplicateSignature);
211 }
212 }
213 }
214
215 let cleartext = self.cleartext(&signer_name, &signer_role);
216 let signature: EdSignature = key.sign(cleartext.as_bytes());
217
218 let ts = SystemTime::now()
220 .duration_since(UNIX_EPOCH)
221 .map_err(|_| SignatureError::SigningFailed)?;
222
223 let signature_entry = Signature {
224 by: signer_name,
225 key: encoded_key,
226 signature: base64::encode(signature.to_bytes()),
227 role: signer_role,
228 at: ts.as_secs(),
229 };
230
231 match self.signature.as_mut() {
232 Some(signatures) => signatures.push(signature_entry),
233 None => self.signature = Some(vec![signature_entry]),
234 };
235
236 Ok(())
237 }
238}
239
240pub fn sign<I>(
250 mut invoice: I,
251 sign_with: Vec<(SignatureRole, &SecretKeyEntry)>,
252) -> Result<SignedInvoice<I>, SignatureError>
253where
254 I: BorrowMut<Invoice> + Into<crate::Invoice>,
255{
256 let inv = invoice.borrow_mut();
257 for (role, key) in sign_with {
258 sign_one(inv, role, key)?;
259 }
260
261 Ok(SignedInvoice(invoice))
262}
263
264fn sign_one(
265 inv: &mut Invoice,
266 signer_role: SignatureRole,
267 keyfile: &SecretKeyEntry,
268) -> Result<(), SignatureError> {
269 let signer_name = keyfile.label.clone();
270 let key = keyfile.key()?;
271 let encoded_key = base64::encode(key.public.to_bytes());
274 if let Some(sigs) = inv.signature.as_ref() {
275 for s in sigs {
276 if s.key == encoded_key {
277 return Err(SignatureError::DuplicateSignature);
278 }
279 }
280 }
281
282 let cleartext = inv.cleartext(&signer_name, &signer_role);
283 let signature: EdSignature = key.sign(cleartext.as_bytes());
284
285 let ts = SystemTime::now()
287 .duration_since(UNIX_EPOCH)
288 .map_err(|_| SignatureError::SigningFailed)?;
289
290 let signature_entry = Signature {
291 by: signer_name,
292 key: encoded_key,
293 signature: base64::encode(signature.to_bytes()),
294 role: signer_role,
295 at: ts.as_secs(),
296 };
297
298 match inv.signature.as_mut() {
299 Some(signatures) => signatures.push(signature_entry),
300 None => inv.signature = Some(vec![signature_entry]),
301 };
302
303 Ok(())
304}
305
306pub struct SignedInvoice<T: Into<Invoice>>(T);
309
310impl<T: Into<Invoice>> Signed for SignedInvoice<T> {
311 fn signed(self) -> Invoice {
312 self.0.into()
313 }
314}
315
316impl<T: Into<Invoice>> sealed::Sealed for SignedInvoice<T> {}
317
318impl<T> Verified for SignedInvoice<T> where T: Verified + Into<Invoice> {}
319
320impl<T> Borrow<Invoice> for SignedInvoice<T>
321where
322 T: Into<Invoice> + Borrow<Invoice>,
323{
324 fn borrow(&self) -> &Invoice {
325 self.0.borrow()
326 }
327}
328
329impl<T> Debug for SignedInvoice<T>
330where
331 T: Debug + Into<Invoice>,
332{
333 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
334 self.0.fmt(f)
335 }
336}
337
338pub(crate) struct NoopSigned<T: Into<Invoice>>(pub(crate) T);
341
342impl<T: Into<Invoice>> Signed for NoopSigned<T> {
343 fn signed(self) -> Invoice {
344 self.0.into()
345 }
346}
347
348impl<T> Verified for NoopSigned<T> where T: Verified + Into<Invoice> {}
349
350impl<T: Into<Invoice>> sealed::Sealed for NoopSigned<T> {}
351
352impl<T> Borrow<Invoice> for NoopSigned<T>
353where
354 T: Into<Invoice> + Borrow<Invoice>,
355{
356 fn borrow(&self) -> &Invoice {
357 self.0.borrow()
358 }
359}
360
361impl<T> Debug for NoopSigned<T>
362where
363 T: Debug + Into<Invoice>,
364{
365 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
366 self.0.fmt(f)
367 }
368}
369
370fn version_compare(version: &Version, requirement: &str) -> bool {
383 if requirement.is_empty() {
384 return true;
385 }
386
387 if let Ok(v) = Version::parse(requirement) {
391 return *version == v;
392 }
393
394 match VersionReq::parse(requirement) {
395 Ok(req) => req.matches(version),
396 Err(e) => {
397 tracing::log::error!("SemVer range could not parse: {}", e);
398 false
399 }
400 }
401}
402
403#[cfg(test)]
404mod test {
405 use super::*;
406 use crate::invoice::signature::{KeyEntry, KeyRing};
407 use std::convert::TryInto;
408 use std::fs::read;
409 use std::path::Path;
410
411 #[test]
412 fn test_version_comparisons() {
413 let reqs = vec!["= 1.2.3", "1.2.3", "1.2.3", "^1.1", "~1.2", ""];
416 let version = Version::parse("1.2.3").unwrap();
417
418 reqs.iter().for_each(|r| {
419 if !version_compare(&version, r) {
420 panic!("Should have passed: {}", r)
421 }
422 });
423
424 let reqs = vec!["2", "%^&%^&%"];
427 reqs.iter()
428 .for_each(|r| assert!(!version_compare(&version, r)));
429 }
430
431 #[test]
432 fn signing_and_verifying() {
433 let invoice = r#"
434 bindleVersion = "1.0.0"
435
436 [bindle]
437 name = "aricebo"
438 version = "1.2.3"
439
440 [[parcel]]
441 [parcel.label]
442 sha256 = "aaabbbcccdddeeefff"
443 name = "telescope.gif"
444 mediaType = "image/gif"
445 size = 123_456
446
447 [[parcel]]
448 [parcel.label]
449 sha256 = "111aaabbbcccdddeee"
450 name = "telescope.txt"
451 mediaType = "text/plain"
452 size = 123_456
453 "#;
454
455 let invoice: crate::Invoice = toml::from_str(invoice).expect("a nice clean parse");
456
457 let signer_name1 = "Matt Butcher <matt@example.com>";
459 let signer_name2 = "Not Matt Butcher <not.matt@example.com>";
460
461 let keypair1 = SecretKeyEntry::new(signer_name1, vec![SignatureRole::Creator]);
462 let keypair2 = SecretKeyEntry::new(signer_name2, vec![SignatureRole::Proxy]);
463
464 let keyring = KeyRing::new(vec![(&keypair1).try_into().expect("convert to public key")]);
467
468 let signed = sign(
470 invoice,
471 vec![
472 (SignatureRole::Creator, &keypair1),
473 (SignatureRole::Proxy, &keypair2),
474 ],
475 )
476 .expect("Sign the parcels");
477
478 let invoice: Invoice = signed.signed();
479
480 sign(invoice.clone(), vec![(SignatureRole::Host, &keypair2)])
482 .expect_err("Should not be able to sign again with the same key");
483
484 assert_eq!(2, invoice.signature.as_ref().unwrap().len());
486
487 VerificationStrategy::CreativeIntegrity
489 .verify(invoice.clone(), &keyring)
490 .expect("with keys on the keyring, this should pass");
491
492 let keyring = KeyRing::new(vec![keypair2.try_into().expect("convert to public key")]);
495 VerificationStrategy::CreativeIntegrity
496 .verify(invoice, &keyring)
497 .expect_err("missing the creator key, so verification should fail");
498 }
499 #[test]
500 fn invalid_signatures_should_fail() {
501 let invoice = r#"
502 bindleVersion = "1.0.0"
503
504 [bindle]
505 name = "aricebo"
506 version = "1.2.3"
507
508 [[signature]]
509 by = "Matt Butcher <matt@example.com>"
510 signature = "T0JWSU9VU0xZIEZBS0UK" # echo "OBVIOUSLY FAKE" | base64
511 key = "jTtZIzQCfZh8xy6st40xxLwxVw++cf0C0cMH3nJBF+c="
512 role = "creator"
513 at = 1611960337
514
515 [[parcel]]
516 [parcel.label]
517 sha256 = "aaabbbcccdddeeefff"
518 name = "telescope.gif"
519 mediaType = "image/gif"
520 size = 123_456
521
522 [[parcel]]
523 [parcel.label]
524 sha256 = "111aaabbbcccdddeee"
525 name = "telescope.txt"
526 mediaType = "text/plain"
527 size = 123_456
528 "#;
529
530 let invoice: crate::Invoice = toml::from_str(invoice).expect("a nice clean parse");
531
532 let pubkey = KeyEntry {
534 key: "jTtZIzQCfZh8xy6st40xxLwxVw++cf0C0cMH3nJBF+c=".to_owned(),
535 label: "Test Key".to_owned(),
536 roles: vec![SignatureRole::Host],
537 label_signature: None,
538 };
539
540 let keyring = KeyRing::new(vec![pubkey]);
542
543 match VerificationStrategy::default().verify(invoice, &keyring) {
544 Err(SignatureError::CorruptSignature(s)) => {
545 assert_eq!("jTtZIzQCfZh8xy6st40xxLwxVw++cf0C0cMH3nJBF+c=", s)
546 }
547 Err(e) => panic!("Unexpected error {:?}", e),
548 Ok(_) => panic!("Verification should have failed"),
549 }
550 }
551
552 #[test]
553 fn invalid_key_should_fail() {
554 let invoice = r#"
555 bindleVersion = "1.0.0"
556
557 [bindle]
558 name = "aricebo"
559 version = "1.2.3"
560
561 [[signature]]
562 by = "Matt Butcher <matt@example.com>"
563 signature = "x6sI2Qme4xf6IRtHGaoMqMRL0vjvVHLq3ZCaKVkHNr3oCw+kvTrxek7RbuajIgS71zUQew4/vVT4Do0xa49+CQ=="
564 key = "T0JWSU9VU0xZIEZBS0UK" # echo "OBVIOUSLY FAKE" | base64
565 role = "creator"
566 at = 1611960337
567
568 [[parcel]]
569 [parcel.label]
570 sha256 = "aaabbbcccdddeeefff"
571 name = "telescope.gif"
572 mediaType = "image/gif"
573 size = 123_456
574
575 [[parcel]]
576 [parcel.label]
577 sha256 = "111aaabbbcccdddeee"
578 name = "telescope.txt"
579 mediaType = "text/plain"
580 size = 123_456
581 "#;
582
583 let invoice: crate::Invoice = toml::from_str(invoice).expect("a nice clean parse");
584
585 let keyring = KeyRing::new(vec![KeyEntry {
587 key: "jTtZIzQCfZh8xy6st40xxLwxVw++cf0C0cMH3nJBF+c=".to_owned(),
588 label: "Test Key".to_owned(),
589 roles: vec![SignatureRole::Creator],
590 label_signature: None,
591 }]);
592
593 match VerificationStrategy::default().verify(invoice, &keyring) {
594 Err(SignatureError::CorruptKey(s)) => assert_eq!("T0JWSU9VU0xZIEZBS0UK", s),
595 Err(e) => panic!("Unexpected error {:?}", e),
596 Ok(_) => panic!("Verification should have failed"),
597 }
598 }
599
600 #[test]
601 fn test_invoice_should_serialize() {
602 let label = Label {
603 sha256: "abcdef1234567890987654321".to_owned(),
604 media_type: "text/toml".to_owned(),
605 name: "foo.toml".to_owned(),
606 size: 101,
607 annotations: None,
608 feature: None,
609 origin: None,
610 };
611 let parcel = Parcel {
612 label,
613 conditions: None,
614 };
615 let parcels = Some(vec![parcel]);
616 let inv = Invoice {
617 bindle_version: crate::BINDLE_VERSION_1.to_owned(),
618 bindle: BindleSpec {
619 id: "foo/1.2.3".parse().unwrap(),
620 description: Some("bar".to_owned()),
621 authors: Some(vec!["m butcher".to_owned()]),
622 },
623 parcel: parcels,
624 yanked: None,
625 yanked_signature: None,
626 annotations: None,
627 group: None,
628 signature: None,
629 };
630
631 let res = toml::to_string(&inv).unwrap();
632 let inv2 = toml::from_str::<Invoice>(res.as_str()).unwrap();
633
634 let b = inv2.bindle;
635 assert_eq!(b.id.name(), "foo".to_owned());
636 assert_eq!(b.id.version_string(), "1.2.3");
637 assert_eq!(b.description.unwrap().as_str(), "bar");
638 assert_eq!(b.authors.unwrap()[0], "m butcher".to_owned());
639
640 let parcels = inv2.parcel.unwrap();
641
642 assert_eq!(parcels.len(), 1);
643
644 let par = &parcels[0];
645 let lab = &par.label;
646 assert_eq!(lab.name, "foo.toml".to_owned());
647 assert_eq!(lab.media_type, "text/toml".to_owned());
648 assert_eq!(lab.sha256, "abcdef1234567890987654321".to_owned());
649 assert_eq!(lab.size, 101);
650 }
651
652 #[test]
653 fn test_examples_in_spec_parse() {
654 let test_files = vec![
655 "test/data/simple-invoice.toml",
656 "test/data/full-invoice.toml",
657 "test/data/alt-format-invoice.toml",
658 ];
659 test_files.iter().for_each(|file| test_parsing_a_file(file));
660 }
661
662 fn test_parsing_a_file(filename: &str) {
663 let invoice_path = Path::new(filename);
664 let raw = read(invoice_path).expect("read file contents");
665
666 let invoice = toml::from_slice::<Invoice>(&raw).expect("clean parse of invoice");
667
668 let _raw2 = toml::to_string_pretty(&invoice).expect("clean serialization of TOML");
670 }
673
674 #[test]
675 fn parcel_no_groups() {
676 let invoice = r#"
677 bindleVersion = "1.0.0"
678
679 [bindle]
680 name = "aricebo"
681 version = "1.2.3"
682
683 [[group]]
684 name = "images"
685
686 [[parcel]]
687 [parcel.label]
688 sha256 = "aaabbbcccdddeeefff"
689 name = "telescope.gif"
690 mediaType = "image/gif"
691 size = 123_456
692 [parcel.conditions]
693 memberOf = ["telescopes"]
694
695 [[parcel]]
696 [parcel.label]
697 sha256 = "111aaabbbcccdddeee"
698 name = "telescope.txt"
699 mediaType = "text/plain"
700 size = 123_456
701 "#;
702
703 let invoice: crate::Invoice = toml::from_str(invoice).expect("a nice clean parse");
704 let parcels = invoice.parcel.expect("expected some parcels");
705
706 let img = &parcels[0];
707 let txt = &parcels[1];
708
709 assert!(img.member_of("telescopes"));
710 assert!(!img.is_global_group());
711
712 assert!(txt.is_global_group());
713 assert!(!txt.member_of("telescopes"));
714 }
715
716 #[test]
717 fn test_group_members() {
718 let invoice = r#"
719 bindleVersion = "1.0.0"
720
721 [bindle]
722 name = "aricebo"
723 version = "1.2.3"
724
725 [[group]]
726 name = "telescopes"
727
728 [[parcel]]
729 [parcel.label]
730 sha256 = "aaabbbcccdddeeefff"
731 name = "telescope.gif"
732 mediaType = "image/gif"
733 size = 123_456
734 [parcel.conditions]
735 memberOf = ["telescopes"]
736
737 [[parcel]]
738 [parcel.label]
739 sha256 = "aaabbbcccdddeeeggg"
740 name = "telescope2.gif"
741 mediaType = "image/gif"
742 size = 123_456
743 [parcel.conditions]
744 memberOf = ["telescopes"]
745
746 [[parcel]]
747 [parcel.label]
748 sha256 = "111aaabbbcccdddeee"
749 name = "telescope.txt"
750 mediaType = "text/plain"
751 size = 123_456
752 "#;
753
754 let invoice: crate::Invoice = toml::from_str(invoice).expect("a nice clean parse");
755 let members = invoice.group_members("telescopes");
756 assert_eq!(2, members.len());
757 }
758}