Skip to main content

sequoia_git/
verify.rs

1//! Commit-tree traversal and verification.
2
3use 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
42/// Whether to trace execution by default (on stderr).
43const 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    // XXX: These should be passed in as arguments.
85    let p: &StandardPolicy = &StandardPolicy::new();
86    let now = std::time::SystemTime::now();
87
88    // STRATEGY
89    //
90    // We want to determine if we can authenticate the target commit
91    // starting from the trust root.  A simple approach is to try each
92    // possible path.  Unfortunately, this is exponential in the
93    // number of merges.  Consider a project where development is done
94    // on feature branches, and then merged using a merge commit.  The
95    // commit graph will look like:
96    //
97    // ```
98    //   o      <- Merge commit
99    //   | \
100    //   |  o   <- Feature branch
101    //   | /
102    //   o      <- Merge commit
103    //   | \
104    //   |  o   <- Feature branch
105    //   | /
106    //   o      <- Merge commit
107    //   | \
108    //   ...
109    // ```
110    //
111    // After 100 such merges, there are 2**100 different paths.
112    // That's intractable.
113    //
114    // Happily, we can do better.  We can determine if there is an
115    // authenticated path as a byproduct of a topological walk.  This
116    // approach limits both the work we have to do, and the space we
117    // require to O(N), where N is the number of commits preceding the
118    // target commit in the commit graph.  (If the trust root is a
119    // dominator, which is usually the case, then N is the number of
120    // commits that precede target, and follow the trust root, which
121    // is often very small.)
122    //
123    // Recall the following facts:
124    //
125    //   - A commit is authenticated if it is the trust root, or at
126    //     least one parent's policy authenticates the commit.
127    //     - Unless the signer's certificate is hard revoked.
128    //       - Unless there is an authenticated suffix that
129    //         immediately follows the commit, and a commit on that
130    //         authenticated suffix goodlists commit.
131    //
132    // In order to check the last condition, we need to know the
133    // goodlists of all the following, authenticated commits when we
134    // process the commit.  When we do a topological walk, we know
135    // that when we visit a commit, we've already visited all of its
136    // ancestors.  This means that we can easily collect the goodlists
137    // of all of the commits on authenticated paths from the commit to
138    // the target during the walk by propagating good lists from
139    // authenticated commits to their parents.  Then, when we are
140    // examining a commit, and we discover that it has been revoked,
141    // we can immediately check if it is on a relevant goodlist.
142    //
143    // To do the topological walk, we first have to do a bit of
144    // preparation.  Since we don't know a commit's children by
145    // looking at the commit (and not all children lead to the
146    // target), we have to extract that information from the graph.
147    // We do this by visiting all commits on paths from the target to
148    // the trust root, which we can do in O(N) space and time by doing
149    // a breath first search.  When we visit a commit, we increment
150    // the number of children each of its parents has.  We also use
151    // this opportunity to discover any certificates that have been
152    // hard revoked.
153    //
154    // Then we do the topological walk.  For each commit, we consider
155    // the number of unprocessed children to be the number of recorded
156    // children.  We then visit commits that have no unprocessed
157    // children.
158    //
159    // When we visit a commit, and there is an authenticated path from
160    // it to the target, we try to authenticate the commit.  That is,
161    // for each parent, we check if the parent can authenticate the
162    // commit.  If a parent authenticates the commit, but the signer's
163    // certificate has been revoked, we can immediately check whether
164    // the commit is on a goodlist.  For each parent that
165    // authenticated the commit, we merge the commit's *aggregate*
166    // goodlist into the parent's goodlist.  Finally, for each parent,
167    // we decrement the number of unprocessed children.  If there are
168    // no unprocessed children left, it is added to a pending queue,
169    // so that it can be processed.
170
171    // Return the policy associated with the commit.
172    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    // Whether we've seen the policy file before.  The key is the
190    // SHA512 digest of the policy file.
191    let mut policy_files: BTreeSet<Vec<u8>> = Default::default();
192    // Any hard revoked certificates.
193    let mut hard_revoked: BTreeSet<Fingerprint> = Default::default();
194
195    // Scans the specified commit's policy for hard revocations, and
196    // adds them to hard_revoked.
197    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        // Scan for revoked certificates.
211        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                // Check if the certificate is hard revoked.
240                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                // Check if any of the signing keys are hard revoked.
254                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        // Number of unprocessed children (commits following this
275        // one).
276        unprocessed_children: usize,
277
278        // Whether there is an authenticated path from this node to
279        // the target.
280        authenticated_suffix: bool,
281
282        // Whether we still need to go via MIDDLE.
283        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    // We walk the tree from the target to the trust root (using a
308    // breath first search, but it doesn't matter), and fill in
309    // COMMITS.
310    let mut saw_trust_root = false;
311    {
312        // Commits that we haven't processed yet.
313        let mut pending: BTreeSet<Oid> = Default::default();
314        pending.insert(commit_range.1.clone());
315
316        // Commits that we've processed.
317        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            // Don't fail if we can't parse the policy file.
325            let _ = scan_policy(commit_id);
326
327            if commit_id == trust_root {
328                // This is the trust root.  There is no need to go
329                // further.
330                saw_trust_root = true;
331                continue;
332            }
333
334            for parent in commit.parents() {
335                let parent_id = parent.id();
336
337                // Add an entry for the parent commit (if we haven't
338                // yet done so).
339                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    // The union of the commit goodlists of commits on an
364    // authenticated suffix (not including this commit).  We build
365    // this up as we authenticate commits.  Since we do a topological
366    // walk, this will be complete for a given commit when we process
367    // that commit.
368    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    // Authenticate the commit using the specified parent.
375    //
376    // NOTE: This must only be called on a commit, if the commit is on
377    // an authenticated suffix!!!
378    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            // This is only the case when verifying the trust root
382            // using the shadow policy.
383            assert_eq!(commit_id, trust_root);
384            assert!(shadow_policy.is_some());
385            None
386        } else {
387            Some(&parent_id)
388        };
389
390        // The current commit's good list.
391        let mut commit_goodlist = BTreeSet::new();
392
393        // XXX: If we have some certificates that are hard revoked,
394        // then we can't use the cache.  This is because the cache
395        // doesn't tell us what certificate was used to sign the
396        // commit, which means we can't figure out if the signer's
397        // certificate was revoked when the result is cached.
398        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            // XXX: communicate this to the caller
421            // instead of println.
422            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                // Whether the parent authenticated the commit.
435                let mut good = false;
436                // Whether the commit was goodlisted by a later
437                // commit.
438                let mut goodlisted = false;
439
440                // Whether the commit was goodlisted by the parent's
441                // policy.  (Because commits form a Merkle tree, this
442                // is only possible when we are using a shadow
443                // policy.)
444                if ! cache_hit && results.is_empty() {
445                    // XXX: communicate this to the caller
446                    // instead of eprintln.
447                    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                            // It looks good, but make sure the
457                            // certificate was not revoked.
458                            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                                // It was revoked, but perhaps the
469                                // commit was goodlisted.
470                                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                // We do NOT insert into the cache if the commit was
499                // goodlisted.  The cache is a function of the parent
500                // policy and the children policy; goodlisting is a
501                // function of commit range.
502                if ! cache_hit && good && ! goodlisted {
503                    cache.insert(&parent_policy, commit_id)?;
504                }
505
506                if cache_hit || good || goodlisted {
507                    // Merge the commit's goodlist into the parent's
508                    // goodlist.
509                    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    // We now do a topological walk from the target to the trust root.
545
546    // Assume there is no path until we prove otherwise.  (A
547    // zero-length path is already valid.)
548    let mut valid_path = trust_root == commit_range.0
549        && commit_range.0 == commit_range.1;
550
551    // Commits that we haven't processed yet and have no unprocessed
552    // children.
553    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                        // This is the trust root and we traversed the
626                        // middle commit.  There is no need to go
627                        // further.
628                        valid_path = true;
629                        if ! keep_going {
630                            break 'authentication;
631                        }
632                    }
633                }
634            }
635        }
636    }
637
638    t!("No commits pending.");
639
640    // When using a shadow policy, we also authenticate the trust
641    // root with it.
642    if (keep_going || valid_path) && shadow_policy.is_some() {
643        t!("Verifying trust root ({}) using the shadow policy",
644           trust_root);
645        // We verify the trust root using itself?  Not quite.  We know
646        // that authenticate_commit prefers the shadow policy, and we
647        // know that a shadow policy is set.  So this will actually
648        // check that the shadow policy verifies the trust root.
649        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    // Look up the tag.
729    let (tag_obj, tag_ref) = git.revparse_ext(tag_name)
730        .with_context(|| {
731            format!("Looking up {:?}.", tag_name)
732        })?;
733
734    // Make sure target references a tag.
735    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        // Get the policy from the commit.
756        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    // Check that we have at least one positive verification.
789    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
840// Returns the SHA512 digest of the provided bytes.
841fn 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    /// Returns whether (policy, commit id) is in the cache.
885    ///
886    /// If (policy, commit id) is in the cache, then it was previously
887    /// determined that the policy authenticated the commit.
888    pub fn contains(&mut self, policy: &[u8], commit: Oid) -> Result<bool> {
889        Ok(self.set.contains(&self.key(policy, commit)?)?)
890    }
891
892    /// Add (policy, commit id) to the cache.
893    ///
894    /// If (policy, commit id) is in the cache, this means that the
895    /// policy considers the commit to be authenticated.  Normally,
896    /// the policy comes from the parent commit, but it may be a
897    /// shadow policy.
898    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}