1use std::{
4 borrow::Cow,
5 path::{Path, PathBuf},
6 collections::{
7 BTreeMap,
8 BTreeSet,
9 },
10};
11
12use anyhow::{anyhow, Context, Result};
13use git2::{
14 Repository,
15 Oid,
16};
17
18use sequoia_openpgp::{
19 self as openpgp,
20 Cert,
21 cert::{
22 amalgamation::ValidAmalgamation,
23 CertParser,
24 },
25 Fingerprint,
26 packet::{Signature, UserID},
27 parse::Parse,
28 policy::StandardPolicy,
29 types::{
30 HashAlgorithm,
31 RevocationStatus,
32 RevocationType,
33 },
34};
35
36use crate::{
37 Error,
38 Policy,
39 persistent_set,
40};
41
42const TRACE: bool = false;
44
45pub trait Output {
46 fn commit(&mut self,
47 commit: &Oid,
48 parent: Option<&Oid>,
49 result: &crate::Result<Vec<crate::Result<(String, Signature, Cert, Fingerprint)>>>)
50 -> crate::Result<()>;
51
52 fn tag(&mut self,
53 tag: &git2::Tag,
54 result: &crate::Result<Vec<crate::Result<(String, Signature, Cert, Fingerprint)>>>)
55 -> crate::Result<()>;
56}
57
58#[derive(Default, Debug)]
59pub struct VerificationResult {
60 pub signer_keys: BTreeSet<openpgp::Fingerprint>,
61 pub primary_uids: BTreeSet<UserID>,
62}
63
64pub fn verify(git: &Repository,
65 trust_root: Oid,
66 shadow_policy: Option<&[u8]>,
67 commit_range: (Oid, Oid),
68 results: &mut VerificationResult,
69 keep_going: bool,
70 mut output: impl Output,
71 cache: &mut VerificationCache,
72 quiet: bool, verbose: bool)
73 -> Result<()>
74{
75 tracer!(TRACE, "verify");
76 t!("verify(_, {}, {}..{})", trust_root, commit_range.0, commit_range.1);
77
78 if shadow_policy.is_some() {
79 t!("Using a shadow policy to verify commits.");
80 } else {
81 t!("Using in-band policies to verify commits.");
82 }
83
84 let p: &StandardPolicy = &StandardPolicy::new();
86 let now = std::time::SystemTime::now();
87
88 let read_policy = |commit: &Oid| -> Result<Cow<[u8]>> {
173 if let Some(p) = &shadow_policy {
174 Ok(Cow::Borrowed(p))
175 } else {
176 match Policy::read_bytes_from_commit(git, commit) {
177 Ok(policy) => Ok(Cow::Owned(policy)),
178 Err(err) => {
179 if let Error::MissingPolicy(_) = err {
180 Ok(Cow::Borrowed(b""))
181 } else {
182 Err(err.into())
183 }
184 }
185 }
186 }
187 };
188
189 let mut policy_files: BTreeSet<Vec<u8>> = Default::default();
192 let mut hard_revoked: BTreeSet<Fingerprint> = Default::default();
194
195 let mut scan_policy = |commit_id| -> Result<()> {
198 let policy_bytes = read_policy(&commit_id)?;
199 let policy_hash = sha512sum(&policy_bytes)?;
200 if policy_files.contains(&policy_hash) {
201 t!("Already scanned an identical copy of {}'s policy, skipping.",
202 commit_id);
203 return Ok(());
204 }
205 t!("Scanning {}'s policy for hard revocations", commit_id);
206
207 let policy = Policy::parse_bytes(&policy_bytes)?;
208 policy_files.insert(policy_hash);
209
210 for authorization in policy.authorization().values() {
212 for cert in CertParser::from_bytes(&authorization.keyring)? {
213 let cert = if let Ok(cert) = cert {
214 cert
215 } else {
216 continue;
217 };
218
219 let vc = if let Ok(vc) = cert.with_policy(p, Some(now)) {
220 vc
221 } else {
222 continue;
223 };
224
225 let is_hard_revoked = |rs| {
226 if let RevocationStatus::Revoked(revs) = rs {
227 revs.iter().any(|rev| {
228 if let Some((reason, _)) = rev.reason_for_revocation() {
229 reason.revocation_type() == RevocationType::Hard
230 } else {
231 true
232 }
233 })
234 } else {
235 false
236 }
237 };
238
239 if is_hard_revoked(vc.revocation_status()) {
241 t!("Certificate {} is hard revoked, bad listing",
242 cert.fingerprint());
243 hard_revoked.insert(vc.fingerprint());
244 for k in vc.keys().subkeys().for_signing() {
245 hard_revoked.insert(k.key().fingerprint());
246 t!(" Badlisting signing key {}",
247 k.key().fingerprint());
248 }
249
250 continue;
251 }
252
253 for k in vc.keys().subkeys().for_signing() {
255 if is_hard_revoked(k.revocation_status()) {
256 hard_revoked.insert(k.key().fingerprint());
257 t!(" Signing key {} hard revoked, bad listing",
258 k.key().fingerprint());
259 }
260 }
261 }
262 }
263
264 Ok(())
265 };
266
267 let middle = if trust_root == commit_range.0 {
268 None
269 } else {
270 Some(commit_range.0)
271 };
272
273 struct Commit {
274 unprocessed_children: usize,
277
278 authenticated_suffix: bool,
281
282 traversed_middle: bool,
284 }
285
286 impl Default for Commit {
287 fn default() -> Self {
288 Commit {
289 unprocessed_children: 0,
290 authenticated_suffix: false,
291 traversed_middle: false,
292 }
293 }
294 }
295
296 let mut commits: BTreeMap<Oid, Commit> = Default::default();
297 if trust_root != commit_range.1 {
298 commits.insert(
299 commit_range.1.clone(),
300 Commit {
301 unprocessed_children: 0,
302 authenticated_suffix: true,
303 traversed_middle: middle.is_none(),
304 });
305 }
306
307 let mut saw_trust_root = false;
311 {
312 let mut pending: BTreeSet<Oid> = Default::default();
314 pending.insert(commit_range.1.clone());
315
316 let mut processed: BTreeSet<Oid> = Default::default();
318
319 while let Some(commit_id) = pending.pop_first() {
320 processed.insert(commit_id);
321
322 let commit = git.find_commit(commit_id)?;
323
324 let _ = scan_policy(commit_id);
326
327 if commit_id == trust_root {
328 saw_trust_root = true;
331 continue;
332 }
333
334 for parent in commit.parents() {
335 let parent_id = parent.id();
336
337 let info = commits.entry(parent_id).or_default();
340 info.unprocessed_children += 1;
341
342 if ! processed.contains(&parent_id)
343 && ! pending.contains(&parent_id)
344 {
345 pending.insert(parent_id);
346 }
347 }
348 }
349 }
350
351 let mut errors = Vec::new();
352
353 if ! saw_trust_root {
354 let err = Error::TrustRootNotAncestor(
355 trust_root.to_string(), commit_range.1.to_string()).into();
356 if keep_going {
357 errors.push(err);
358 } else {
359 return Err(err);
360 }
361 }
362
363 let mut descendant_goodlist: BTreeMap<Oid, BTreeSet<String>>
369 = Default::default();
370
371 let mut unauthenticated_commits: BTreeSet<Oid> = Default::default();
372 let mut authenticated_commits: BTreeSet<Oid> = Default::default();
373
374 let mut authenticate_commit = |commit_id, parent_id| -> Result<bool> {
379 let parent_policy = read_policy(&parent_id)?;
380 let parent_id = if commit_id == parent_id {
381 assert_eq!(commit_id, trust_root);
384 assert!(shadow_policy.is_some());
385 None
386 } else {
387 Some(&parent_id)
388 };
389
390 let mut commit_goodlist = BTreeSet::new();
392
393 let (vresult, cache_hit) = if hard_revoked.is_empty()
399 && cache.contains(&parent_policy, commit_id)?
400 {
401 (Ok(vec![]), true)
402 } else {
403 let parent_policy = Policy::parse_bytes(&parent_policy)?;
404 let commit_policy = Policy::parse_bytes(read_policy(&commit_id)?)?;
405
406 commit_goodlist = commit_policy.commit_goodlist().clone();
407
408 (parent_policy.verify(git, &commit_id, &commit_policy,
409 &mut results.signer_keys,
410 &mut results.primary_uids),
411 false)
412 };
413
414 if let Err(err) = output.commit(&commit_id, parent_id, &vresult) {
415 t!("verify_cb -> {}", err);
416 return Err(err.into());
417 }
418
419 if cache_hit {
420 let id = if let Some(parent_id) = parent_id {
423 format!("{}..{}", parent_id, commit_id)
424 } else {
425 commit_id.to_string()
426 };
427 if verbose {
428 println!("{}:\n Cached positive verification", id);
429 }
430 }
431
432 match vresult {
433 Ok(results) => {
434 let mut good = false;
436 let mut goodlisted = false;
439
440 if ! cache_hit && results.is_empty() {
445 if verbose {
448 println!("{}: Explicitly goodlisted", commit_id);
449 }
450 good = true;
451 }
452
453 for r in results {
454 match r {
455 Ok((_, _sig, cert, signer_fpr)) => {
456 if hard_revoked.contains(&signer_fpr) {
459 t!("Cert {}{} used to sign {} is revoked.",
460 cert.fingerprint(),
461 if cert.fingerprint() != signer_fpr {
462 format!(", key {}", signer_fpr)
463 } else {
464 "".to_string()
465 },
466 commit_id);
467
468 if descendant_goodlist.get(&commit_id)
471 .map(|goodlist| {
472 t!(" Goodlist contains: {}",
473 goodlist
474 .iter().cloned()
475 .collect::<Vec<String>>()
476 .join(", "));
477 goodlist.contains(&commit_id.to_string())
478 })
479 .unwrap_or(false)
480 {
481 t!("But the commit was goodlisted, \
482 so all is good.");
483 goodlisted = true;
484 }
485 } else {
486 t!("{} has a good signature from {}",
487 commit_id, cert.fingerprint());
488 good = true;
489 }
490 }
491 Err(e) => errors.push(
492 anyhow::Error::from(e).context(
493 format!("While verifying commit {}",
494 commit_id))),
495 }
496 }
497
498 if ! cache_hit && good && ! goodlisted {
503 cache.insert(&parent_policy, commit_id)?;
504 }
505
506 if cache_hit || good || goodlisted {
507 if let Some(descendant_goodlist)
510 = descendant_goodlist.get(&commit_id)
511 {
512 commit_goodlist.extend(descendant_goodlist.iter().cloned());
513 };
514
515 if let Some(parent_id) = parent_id {
516 if let Some(p_goodlist)
517 = descendant_goodlist.get_mut(&parent_id)
518 {
519 p_goodlist.extend(commit_goodlist.into_iter());
520 } else if ! commit_goodlist.is_empty() {
521 descendant_goodlist.insert(
522 parent_id.clone(), commit_goodlist);
523 }
524 }
525 }
526
527 let authenticated = cache_hit || good || goodlisted;
528 if authenticated {
529 authenticated_commits.insert(commit_id);
530 } else {
531 unauthenticated_commits.insert(commit_id);
532 }
533 Ok(authenticated)
534 },
535 Err(e) => {
536 unauthenticated_commits.insert(commit_id);
537 errors.push(anyhow::Error::from(e).context(
538 format!("While verifying commit {}", commit_id)));
539 Ok(false)
540 },
541 }
542 };
543
544 let mut valid_path = trust_root == commit_range.0
549 && commit_range.0 == commit_range.1;
550
551 let mut pending: BTreeSet<Oid> = Default::default();
554 if trust_root != commit_range.1 {
555 pending.insert(commit_range.1.clone());
556 }
557
558 'authentication: while let Some(commit_id) = pending.pop_first() {
559 let commit = git.find_commit(commit_id)?;
560
561 t!("Processing {}: {}", commit_id, commit.summary().unwrap_or(""));
562
563 let commit_info = commits.get(&commit_id).expect("added");
564 assert_eq!(commit_info.unprocessed_children, 0);
565 let authenticated_suffix = commit_info.authenticated_suffix;
566 let traversed_middle = commit_info.traversed_middle;
567
568 for (parent_i, parent) in commit.parents().enumerate() {
569 let parent_id = parent.id();
570 t!("Considering {} -> {} (parent #{} of {})",
571 commit_id, parent_id, parent_i + 1, commit.parents().len());
572
573 let parent_is_trust_root = parent_id == trust_root;
574
575 let parent_info = commits.get_mut(&parent_id)
576 .with_context(|| format!("Looking up {}", parent_id))
577 .expect("added");
578 t!(" Parent has {} unprocessed children",
579 parent_info.unprocessed_children);
580 assert!(parent_info.unprocessed_children > 0);
581 parent_info.unprocessed_children -= 1;
582 if parent_info.unprocessed_children == 0 && ! parent_is_trust_root {
583 t!(" Adding parent to pending queue for processing");
584 pending.insert(parent_id);
585 }
586
587 if authenticated_suffix {
588 t!(" Child IS on an authenticated suffix");
589 } else {
590 t!(" Child IS NOT on an authenticated suffix.");
591 }
592
593 let authenticated = if keep_going || authenticated_suffix {
594 authenticate_commit(commit_id, parent_id)
595 .with_context(|| {
596 format!("Authenticating {} with {}",
597 commit_id, parent_id)
598 })?
599 } else {
600 false
601 };
602 t!(" Could {}authenticate commit.",
603 if authenticated { "" } else { "not " });
604
605 if authenticated_suffix && authenticated {
606 t!(" Parent authenticates child");
607 if ! parent_info.authenticated_suffix {
608 t!(" Parent is now part of an authenticated suffix.");
609 }
610 parent_info.authenticated_suffix = true;
611
612 if traversed_middle {
613 parent_info.traversed_middle = true;
614 } else if middle == Some(commit_id) {
615 t!(" Traversed {} on way to trust root.", commit_id);
616 parent_info.traversed_middle = true;
617 }
618
619 if parent_is_trust_root {
620 t!(" Parent is the trust root.");
621
622 if ! parent_info.traversed_middle {
623 t!(" but path was not via {}", middle.unwrap());
624 } else {
625 valid_path = true;
629 if ! keep_going {
630 break 'authentication;
631 }
632 }
633 }
634 }
635 }
636 }
637
638 t!("No commits pending.");
639
640 if (keep_going || valid_path) && shadow_policy.is_some() {
643 t!("Verifying trust root ({}) using the shadow policy",
644 trust_root);
645 if ! authenticate_commit(trust_root, trust_root)? {
650 valid_path = false;
651
652 if let Some(e) = errors.pop() {
653 errors.push(
654 e.context(format!("Could not verify trust root {} \
655 using the specified policy",
656 trust_root)));
657 }
658 }
659 }
660
661 if valid_path {
662 if trust_root == commit_range.0 {
663 if ! quiet {
664 println!(
665 "Verified that there is an authenticated path from the trust root\n\
666 {} to {}.",
667 trust_root, commit_range.1);
668 }
669 } else {
670 if ! quiet {
671 println!(
672 "Verified that there is an authenticated path from the trust root\n\
673 {} via {}\n\
674 to {}.",
675 trust_root, commit_range.0, commit_range.1);
676 }
677 }
678 Ok(())
679 } else {
680 if errors.is_empty() {
681 Err(anyhow!("Could not verify commits {}..{}{}",
682 trust_root,
683 if let Some(middle) = middle {
684 format!("{}..", middle)
685 } else {
686 "".to_string()
687 },
688 commit_range.1))
689 } else {
690 let mut e = errors.swap_remove(0)
691 .context(format!("Could not verify commits {}..{}{}",
692 commit_range.0,
693 if let Some(middle) = middle {
694 format!("{}..", middle)
695 } else {
696 "".to_string()
697 },
698 commit_range.1));
699 if ! errors.is_empty() {
700 e = e.context(
701 format!("{} errors occurred while verifying the commits. \
702 {} commits couldn't be authenticated. \
703 Note: not all errors are fatal. \
704 The first error is shown:",
705 errors.len() + 1,
706 unauthenticated_commits.difference(
707 &authenticated_commits).count()));
708 }
709 Err(e)
710 }
711 }
712}
713
714pub fn verify_tag(git: &Repository,
715 trust_root: Oid,
716 shadow_policy: Option<&[u8]>,
717 tag_name: &str,
718 vresults: &mut VerificationResult,
719 keep_going: bool,
720 mut output: impl Output,
721 cache: &mut VerificationCache,
722 quiet: bool, verbose: bool)
723 -> Result<()>
724{
725 tracer!(TRACE, "verify_tag");
726 t!("verify(_, {}, {})", trust_root, tag_name);
727
728 let (tag_obj, tag_ref) = git.revparse_ext(tag_name)
730 .with_context(|| {
731 format!("Looking up {:?}.", tag_name)
732 })?;
733
734 let Some(tag_ref) = tag_ref else {
736 return Err(Error::NotATag(tag_name.into()).into());
737 };
738
739 let Some(tag) = tag_obj.as_tag() else {
740 return Err(Error::NotATag(tag_name.into()).into());
741 };
742
743 let Some(tag_oid) = tag_ref.target() else {
744 return Err(Error::CannotResolve(tag_name.into()).into());
745 };
746
747 let target_commit = tag_ref.peel_to_commit()
748 .with_context(|| {
749 format!("{:?} does not resolve to a commit", tag_name)
750 })?;
751
752 let policy = if let Some(shadow_policy) = shadow_policy {
753 Cow::Borrowed(shadow_policy)
754 } else {
755 match Policy::read_bytes_from_commit(&git, &target_commit.id()) {
757 Ok(policy) => {
758 Cow::Owned(policy)
759 }
760 Err(err) => {
761 if let Error::MissingPolicy(_) = err {
762 Cow::Borrowed(&b""[..])
763 } else {
764 return Err(anyhow::Error::from(err))
765 .with_context(|| {
766 format!("Reading policy from {}",
767 target_commit.id())
768 });
769 }
770 }
771 }
772 };
773
774 let policy = Policy::parse_bytes(policy)
775 .context("Parsing policy")?;
776
777 let results = policy.verify_tag(
778 &git, tag_oid,
779 &policy,
780 &mut vresults.signer_keys,
781 &mut vresults.primary_uids);
782
783 if let Err(err) = output.tag(&tag, &results) {
784 t!("verify_tag_cb -> {}", err);
785 return Err(err.into());
786 }
787
788 let mut pending_error = None;
790 let mut ok = Vec::new();
791 match results {
792 Ok(results) => {
793 for r in results.into_iter() {
794 match r {
795 Err(err) => {
796 if pending_error.is_none() {
797 pending_error = Some(err);
798 }
799 }
800 Ok((id, _, _, _)) => {
801 ok.push(id)
802 }
803 }
804 }
805 }
806 Err(e) => {
807 return Err(e.into());
808 }
809 }
810
811 if ! ok.is_empty() {
812 if ! quiet {
813 println!("Tag {} was signed by {}",
814 tag_name, ok.join(", "));
815 }
816 } else {
817 if ! keep_going {
818 if let Some(err) = pending_error {
819 return Err(err.into());
820 }
821 }
822 }
823
824 verify(&git, trust_root, shadow_policy,
825 (trust_root, target_commit.id()),
826 vresults,
827 keep_going,
828 output,
829 cache,
830 quiet, verbose,
831 )?;
832
833 if let Some(err) = pending_error {
834 Err(err.into())
835 } else {
836 Ok(())
837 }
838}
839
840fn sha512sum(bytes: &[u8]) -> Result<Vec<u8>> {
842 let mut digest = HashAlgorithm::SHA512.context()?.for_digest();
843 digest.update(bytes);
844
845 let mut key = vec![0; 32];
846 digest.digest(&mut key)?;
847 Ok(key)
848}
849
850pub struct VerificationCache {
851 path: PathBuf,
852 set: persistent_set::Set,
853}
854
855impl VerificationCache {
856 const CONTEXT: &'static str = "SqGitVerify0";
857
858 pub fn new() -> Result<Self> {
859 let p = dirs::cache_dir().ok_or(anyhow::anyhow!("No cache dir"))?
860 .join("sq-git.verification.cache");
861 Self::open(p)
862 }
863
864 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
865 let path = path.as_ref();
866
867 Ok(VerificationCache {
868 path: path.into(),
869 set: persistent_set::Set::read(&path, Self::CONTEXT)?,
870 })
871 }
872
873 fn key(&self, policy: &[u8], commit: Oid) -> Result<persistent_set::Value> {
874 let mut digest = HashAlgorithm::SHA512.context()?.for_digest();
875 digest.update(policy);
876 digest.update(commit.as_bytes());
877
878 let mut key = [0; 32];
879 digest.digest(&mut key)?;
880
881 Ok(key)
882 }
883
884 pub fn contains(&mut self, policy: &[u8], commit: Oid) -> Result<bool> {
889 Ok(self.set.contains(&self.key(policy, commit)?)?)
890 }
891
892 pub fn insert(&mut self, policy: &[u8], commit: Oid) -> Result<()> {
899 self.set.insert(self.key(policy, commit)?);
900 Ok(())
901 }
902
903 pub fn persist(&mut self) -> Result<()> {
904 self.set.write(&self.path)?;
905 Ok(())
906 }
907}