Skip to main content

gix_object/signature/
verify.rs

1use std::{
2    // defensive, as we rely on English when parsing output.
3    ffi::{OsStr, OsString},
4    io::Write,
5    path::PathBuf,
6    process::Stdio,
7};
8
9use bstr::{BStr, BString, ByteSlice};
10
11use super::SignedData;
12
13use super::Format;
14
15/// Fully resolved options for verifying a commit or annotated-tag signature.
16#[derive(Clone, Debug)]
17pub enum Options {
18    /// Verify an OpenPGP signature.
19    OpenPgp {
20        /// The external verification program or command.
21        program: OsString,
22        /// Additional arguments passed before Git's fixed arguments.
23        ///
24        /// Useful for selecting an alternate key store or configuring a wrapper around the verifier.
25        program_arguments: Vec<OsString>,
26        /// Environment variables set only for the verifier.
27        environment: Vec<(OsString, OsString)>,
28        /// The minimum trust required for [`Outcome::is_valid()`] to return `true`.
29        minimum_trust: TrustLevel,
30    },
31    /// Verify an X.509 signature.
32    X509 {
33        /// The external verification program or command.
34        program: OsString,
35        /// Additional arguments passed before Git's fixed arguments, useful for selecting an alternate key store or
36        /// configuring a wrapper around the verifier.
37        program_arguments: Vec<OsString>,
38        /// Environment variables set only for the verifier.
39        environment: Vec<(OsString, OsString)>,
40        /// The minimum trust required for [`Outcome::is_valid()`] to return `true`.
41        minimum_trust: TrustLevel,
42    },
43    /// Verify an SSH signature.
44    Ssh {
45        /// The external verification program or command.
46        program: OsString,
47        /// Additional arguments passed before Git's fixed arguments, useful for selecting an alternate key store or
48        /// configuring a wrapper around the verifier.
49        program_arguments: Vec<OsString>,
50        /// Environment variables set only for the verifier.
51        environment: Vec<(OsString, OsString)>,
52        /// The allowed-signers file.
53        allowed_signers: PathBuf,
54        /// An optional revocation file.
55        revocation_file: Option<PathBuf>,
56        /// The signature creation time passed to `ssh-keygen` as `-Overify-time` when evaluating `valid-after` and
57        /// `valid-before` constraints in the allowed-signers file.
58        ///
59        /// For commits this should be the committer timestamp, so key rotation or expiry does not invalidate a
60        /// signature created while the key was authorized.
61        verify_time: gix_date::Time,
62        /// The minimum trust required for [`Outcome::is_valid()`] to return `true`.
63        minimum_trust: TrustLevel,
64    },
65}
66
67/// The result reported by the signature verifier.
68#[derive(Clone, Copy, Debug, Eq, PartialEq)]
69pub enum Status {
70    /// The signature is cryptographically valid.
71    Good,
72    /// The signature is invalid.
73    Bad,
74    /// The verifier could not check the signature.
75    Error,
76    /// The signature has expired.
77    Expired,
78    /// The signing key has expired.
79    ExpiredKey,
80    /// The signing key was revoked.
81    RevokedKey,
82    /// The verifier returned no recognized result.
83    Unknown,
84}
85
86/// The trust level reported by the signature verifier.
87#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
88pub enum TrustLevel {
89    /// No trust information is available.
90    #[default]
91    Undefined,
92    /// The key must never be trusted.
93    Never,
94    /// The verifier considers the signing key's claimed identity marginally valid under its configured trust model.
95    ///
96    /// The signature may be cryptographically correct, but the verifier has less confidence in the association between
97    /// the key and its claimed identity than at [`TrustLevel::Fully`].
98    Marginal,
99    /// The verifier considers the signing key's claimed identity fully valid under its configured trust model.
100    ///
101    /// This describes confidence in the key-to-identity association, not greater cryptographic strength. For SSH
102    /// signatures, this implementation reports this level when the signature is valid for a principal found in the
103    /// allowed-signers file.
104    Fully,
105    /// The verifier's highest trust level for the signing key's claimed identity.
106    ///
107    /// For OpenPGP this commonly identifies one's own key or a key explicitly granted ultimate trust. This
108    /// implementation does not assign this level to SSH signatures.
109    Ultimate,
110}
111
112/// The complete result of verifying a commit or annotated-tag signature.
113#[derive(Clone, Debug, Eq, PartialEq)]
114pub struct Outcome {
115    /// The detected signature format and therefore the verifier whose output populated the remaining fields.
116    pub format: Format,
117    /// The cryptographic status parsed from GPG/GPGSM status records or from `ssh-keygen`'s success output.
118    pub status: Status,
119    /// The trust reported by GPG/GPGSM. For SSH this is [`TrustLevel::Fully`] for a principal in the allowed-signers
120    /// file and [`TrustLevel::Undefined`] for an otherwise valid signature from an unknown key.
121    pub trust_level: TrustLevel,
122    /// The GPG/GPGSM user ID or the SSH principal found in the allowed-signers file, if available.
123    pub signer: Option<BString>,
124    /// The GPG/GPGSM key ID. For SSH this is the same value as [`Outcome::fingerprint`].
125    pub key: Option<BString>,
126    /// The GPG/GPGSM signing-key fingerprint or the fingerprint printed by `ssh-keygen`, if available.
127    pub fingerprint: Option<BString>,
128    /// The GPG/GPGSM primary-key fingerprint, if reported; always `None` for SSH.
129    pub primary_key_fingerprint: Option<BString>,
130    /// Human-readable GPG/GPGSM stderr, or the available `ssh-keygen` stdout and stderr concatenated in that order.
131    pub output: BString,
132    /// GPG/GPGSM `--status-fd` output. SSH has no separate machine-readable channel, so this equals [`Outcome::output`].
133    pub raw_output: BString,
134    valid: bool,
135}
136
137impl TrustLevel {
138    /// Parse a Git trust-level name case-insensitively, or return `None` if it is unknown.
139    pub fn from_bytes(value: &[u8]) -> Option<Self> {
140        if value.eq_ignore_ascii_case(b"undefined") {
141            Some(TrustLevel::Undefined)
142        } else if value.eq_ignore_ascii_case(b"never") {
143            Some(TrustLevel::Never)
144        } else if value.eq_ignore_ascii_case(b"marginal") {
145            Some(TrustLevel::Marginal)
146        } else if value.eq_ignore_ascii_case(b"fully") {
147            Some(TrustLevel::Fully)
148        } else if value.eq_ignore_ascii_case(b"ultimate") {
149            Some(TrustLevel::Ultimate)
150        } else {
151            None
152        }
153    }
154}
155
156impl Outcome {
157    /// Return `true` if Git would accept the signature with the configured minimum trust level.
158    pub fn is_valid(&self) -> bool {
159        self.valid
160    }
161}
162
163/// The error returned when verifying an object signature.
164#[derive(Debug, thiserror::Error)]
165#[expect(missing_docs)]
166pub enum Error {
167    #[error("The signature format is unsupported")]
168    UnsupportedFormat,
169    #[error("The configured program format {program_format:?} does not match signature format {signature_format:?}")]
170    FormatMismatch {
171        program_format: Format,
172        signature_format: Format,
173    },
174    #[error("Could not create or write the temporary signature file")]
175    TemporaryFile(#[source] std::io::Error),
176    #[error("Could not execute signature verifier {program:?}")]
177    Spawn { program: OsString, source: std::io::Error },
178    #[error("Could not communicate with signature verifier {program:?}")]
179    Communicate { program: OsString, source: std::io::Error },
180    #[error("Signature time could not be formatted for SSH verification")]
181    CommitTime(#[source] Box<dyn std::error::Error + Send + Sync>),
182}
183
184impl SignedData<'_> {
185    /// Verify `signature` over these exact object bytes with fully resolved `options`.
186    pub fn verify(&self, signature: &BStr, options: Options) -> Result<Outcome, Error> {
187        let format = Format::from_signature(signature).ok_or(Error::UnsupportedFormat)?;
188        match options {
189            Options::OpenPgp {
190                program,
191                program_arguments,
192                environment,
193                minimum_trust,
194            } if format == Format::OpenPgp => self.verify_gpg(
195                signature,
196                Format::OpenPgp,
197                program,
198                program_arguments,
199                environment,
200                minimum_trust,
201            ),
202            Options::X509 {
203                program,
204                program_arguments,
205                environment,
206                minimum_trust,
207            } if format == Format::X509 => self.verify_gpg(
208                signature,
209                Format::X509,
210                program,
211                program_arguments,
212                environment,
213                minimum_trust,
214            ),
215            Options::Ssh {
216                program,
217                program_arguments,
218                environment,
219                allowed_signers,
220                revocation_file,
221                verify_time,
222                minimum_trust,
223            } if format == Format::Ssh => self.verify_ssh(
224                signature,
225                program,
226                program_arguments,
227                environment,
228                allowed_signers,
229                revocation_file,
230                verify_time,
231                minimum_trust,
232            ),
233            Options::OpenPgp { .. } => Err(Error::FormatMismatch {
234                program_format: Format::OpenPgp,
235                signature_format: format,
236            }),
237            Options::X509 { .. } => Err(Error::FormatMismatch {
238                program_format: Format::X509,
239                signature_format: format,
240            }),
241            Options::Ssh { .. } => Err(Error::FormatMismatch {
242                program_format: Format::Ssh,
243                signature_format: format,
244            }),
245        }
246    }
247
248    fn verify_gpg(
249        &self,
250        signature: &BStr,
251        format: Format,
252        program: OsString,
253        program_arguments: Vec<OsString>,
254        environment: Vec<(OsString, OsString)>,
255        minimum_trust: TrustLevel,
256    ) -> Result<Outcome, Error> {
257        let mut signature_file = signature_file(signature)?;
258        let path = signature_path(&mut signature_file)?;
259        let mut command = prepare(&program, program_arguments, &environment);
260        if format == Format::OpenPgp {
261            command = command.arg("--keyid-format=long");
262        }
263        let command = command.args(["--status-fd=1", "--verify"]).arg(path);
264        let output = if format == Format::X509 {
265            let mut signed_file = temporary_file(self.segments())?;
266            let signed_path = signature_path(&mut signed_file)?;
267            run_without_input(
268                command
269                    .arg(signed_path)
270                    .stdin(Stdio::null())
271                    .stdout(Stdio::piped())
272                    .stderr(Stdio::piped()),
273                &program,
274            )?
275        } else {
276            self.run(
277                command
278                    .arg("-")
279                    .stdin(Stdio::piped())
280                    .stdout(Stdio::piped())
281                    .stderr(Stdio::piped()),
282                &program,
283            )?
284        };
285        let mut outcome = parse_gpg_output(format, output.stderr.into(), output.stdout.into());
286        outcome.valid =
287            output.status.success() && outcome.status == Status::Good && outcome.trust_level >= minimum_trust;
288        Ok(outcome)
289    }
290
291    #[expect(clippy::too_many_arguments)]
292    fn verify_ssh(
293        &self,
294        signature: &BStr,
295        program: OsString,
296        program_arguments: Vec<OsString>,
297        mut environment: Vec<(OsString, OsString)>,
298        allowed_signers: PathBuf,
299        revocation_file: Option<PathBuf>,
300        verify_time: gix_date::Time,
301        minimum_trust: TrustLevel,
302    ) -> Result<Outcome, Error> {
303        let verify_time = verify_time
304            .format(gix_date::time::CustomFormat::new("%Y%m%d%H%M%S"))
305            .map_err(|err| Error::CommitTime(Box::new(err)))?;
306        let verify_time = format!("-Overify-time={verify_time}");
307        let mut signature_file = signature_file(signature)?;
308        let signature_path = signature_path(&mut signature_file)?;
309        // defensive, as we rely on English when parsing output.
310        environment.extend([("LANG".into(), "C".into()), ("LC_ALL".into(), "C".into())]);
311        let common = (
312            program.as_os_str(),
313            program_arguments.as_slice(),
314            environment.as_slice(),
315        );
316        let principals = run_prepared(
317            common,
318            [
319                "-Y".into(),
320                "find-principals".into(),
321                "-f".into(),
322                allowed_signers.as_os_str().into(),
323                "-s".into(),
324                signature_path.as_os_str().into(),
325                verify_time.as_str().into(),
326            ],
327            &[],
328        )?;
329        let mut final_output = None;
330        let mut signer = None;
331        if principals.status.success() {
332            for principal in principals.stdout.lines().filter(|line| !line.trim().is_empty()) {
333                let principal = OsString::from(String::from_utf8_lossy(principal.trim()).as_ref());
334                let mut args = vec![
335                    "-Y".into(),
336                    "verify".into(),
337                    "-n".into(),
338                    "git".into(),
339                    "-f".into(),
340                    allowed_signers.as_os_str().into(),
341                    "-I".into(),
342                    principal.clone(),
343                    "-s".into(),
344                    signature_path.as_os_str().into(),
345                    verify_time.as_str().into(),
346                ];
347                if let Some(revocation_file) = &revocation_file {
348                    args.extend(["-r".into(), revocation_file.as_os_str().into()]);
349                }
350                let output = self.run_prepared(common, args)?;
351                if output.status.success() && output.stdout.starts_with(b"Good") {
352                    signer = Some(principal.to_string_lossy().as_bytes().into());
353                    final_output = Some(output);
354                    break;
355                }
356                final_output = Some(output);
357            }
358        }
359        let (output, trust_level, command_success) = match final_output {
360            Some(output) => {
361                let command_success = output.status.success();
362                (output, TrustLevel::Fully, command_success)
363            }
364            None => (
365                self.run_prepared(
366                    common,
367                    [
368                        "-Y".into(),
369                        "check-novalidate".into(),
370                        "-n".into(),
371                        "git".into(),
372                        "-s".into(),
373                        signature_path.as_os_str().into(),
374                        verify_time.as_str().into(),
375                    ],
376                )?,
377                TrustLevel::Undefined,
378                false,
379            ),
380        };
381        let human = if output.stdout.is_empty() {
382            output.stderr
383        } else if output.stderr.is_empty() {
384            output.stdout
385        } else {
386            [output.stdout, output.stderr].concat()
387        };
388        let mut outcome = parse_ssh_output(human.into(), signer, trust_level);
389        outcome.valid = command_success && outcome.status == Status::Good && trust_level >= minimum_trust;
390        Ok(outcome)
391    }
392
393    fn run(&self, command: gix_command::Prepare, program: &OsStr) -> Result<std::process::Output, Error> {
394        let mut child = command.spawn().map_err(|source| Error::Spawn {
395            program: program.to_owned(),
396            source,
397        })?;
398        let mut stdin = child.stdin.take().expect("configured as piped");
399        let [before, after] = self.segments();
400        if let Err(source) = stdin.write_all(before).and_then(|_| stdin.write_all(after)) {
401            // A verifier may reject the invocation and exit without consuming all input. Its status and output are
402            // still authoritative, whereas other write failures indicate an actual communication problem.
403            if source.kind() != std::io::ErrorKind::BrokenPipe {
404                return Err(Error::Communicate {
405                    program: program.to_owned(),
406                    source,
407                });
408            }
409        }
410        drop(stdin);
411        child.wait_with_output().map_err(|source| Error::Communicate {
412            program: program.to_owned(),
413            source,
414        })
415    }
416
417    fn run_prepared(
418        &self,
419        common: (&OsStr, &[OsString], &[(OsString, OsString)]),
420        args: impl IntoIterator<Item = OsString>,
421    ) -> Result<std::process::Output, Error> {
422        let (program, program_arguments, environment) = common;
423        self.run(
424            prepare(program, program_arguments.iter().cloned(), environment)
425                .args(args)
426                .stdin(Stdio::piped())
427                .stdout(Stdio::piped())
428                .stderr(Stdio::piped()),
429            program,
430        )
431    }
432}
433
434fn prepare(
435    program: &OsStr,
436    arguments: impl IntoIterator<Item = OsString>,
437    environment: &[(OsString, OsString)],
438) -> gix_command::Prepare {
439    environment.iter().fold(
440        gix_command::prepare(program)
441            .command_may_be_shell_script()
442            .args(arguments),
443        |command, (key, value)| command.env(key, value),
444    )
445}
446
447fn run_prepared(
448    common: (&OsStr, &[OsString], &[(OsString, OsString)]),
449    args: impl IntoIterator<Item = OsString>,
450    input: &[u8],
451) -> Result<std::process::Output, Error> {
452    let (program, program_arguments, environment) = common;
453    let command = prepare(program, program_arguments.iter().cloned(), environment)
454        .args(args)
455        .stdin(Stdio::piped())
456        .stdout(Stdio::piped())
457        .stderr(Stdio::piped());
458    let mut child = command.spawn().map_err(|source| Error::Spawn {
459        program: program.to_owned(),
460        source,
461    })?;
462    child
463        .stdin
464        .take()
465        .expect("configured as piped")
466        .write_all(input)
467        .map_err(|source| Error::Communicate {
468            program: program.to_owned(),
469            source,
470        })?;
471    child.wait_with_output().map_err(|source| Error::Communicate {
472        program: program.to_owned(),
473        source,
474    })
475}
476
477fn signature_file(signature: &BStr) -> Result<gix_tempfile::Handle<gix_tempfile::handle::Writable>, Error> {
478    temporary_file([signature.as_ref()])
479}
480
481fn temporary_file<'a>(
482    data: impl IntoIterator<Item = &'a [u8]>,
483) -> Result<gix_tempfile::Handle<gix_tempfile::handle::Writable>, Error> {
484    let mut file = gix_tempfile::new(
485        std::env::temp_dir(),
486        gix_tempfile::ContainingDirectory::Exists,
487        gix_tempfile::AutoRemove::Tempfile,
488    )
489    .map_err(Error::TemporaryFile)?;
490    file.with_mut(|file| {
491        for data in data {
492            file.write_all(data)?;
493        }
494        Ok(())
495    })
496    .map_err(Error::TemporaryFile)?
497    .map_err(Error::TemporaryFile)?;
498    Ok(file)
499}
500
501fn signature_path(file: &mut gix_tempfile::Handle<gix_tempfile::handle::Writable>) -> Result<PathBuf, Error> {
502    file.with_mut(|file| file.path().to_owned())
503        .map_err(Error::TemporaryFile)
504}
505
506fn run_without_input(command: gix_command::Prepare, program: &OsStr) -> Result<std::process::Output, Error> {
507    command
508        .spawn()
509        .map_err(|source| Error::Spawn {
510            program: program.to_owned(),
511            source,
512        })?
513        .wait_with_output()
514        .map_err(|source| Error::Communicate {
515            program: program.to_owned(),
516            source,
517        })
518}
519
520fn parse_gpg_output(format: Format, output: BString, raw_output: BString) -> Outcome {
521    let mut outcome = Outcome {
522        format,
523        status: Status::Unknown,
524        trust_level: TrustLevel::Undefined,
525        signer: None,
526        key: None,
527        fingerprint: None,
528        primary_key_fingerprint: None,
529        output,
530        raw_output,
531        valid: false,
532    };
533    let mut exclusive = false;
534    for line in outcome.raw_output.lines() {
535        let Some(line) = line.strip_prefix(b"[GNUPG:] ") else {
536            continue;
537        };
538        for (prefix, status) in [
539            (b"GOODSIG ".as_slice(), Status::Good),
540            (b"BADSIG ".as_slice(), Status::Bad),
541            (b"ERRSIG ".as_slice(), Status::Error),
542            (b"EXPSIG ".as_slice(), Status::Expired),
543            (b"EXPKEYSIG ".as_slice(), Status::ExpiredKey),
544            (b"REVKEYSIG ".as_slice(), Status::RevokedKey),
545        ] {
546            if let Some(value) = line.strip_prefix(prefix) {
547                if exclusive {
548                    outcome.status = Status::Error;
549                    outcome.signer = None;
550                    outcome.key = None;
551                    break;
552                }
553                exclusive = true;
554                outcome.status = status;
555                let mut fields = value.splitn(2, |byte| *byte == b' ');
556                outcome.key = fields.next().filter(|value| !value.is_empty()).map(BString::from);
557                outcome.signer = fields.next().filter(|value| !value.is_empty()).map(BString::from);
558                break;
559            }
560        }
561        if let Some(value) = line.strip_prefix(b"TRUST_") {
562            outcome.trust_level = TrustLevel::from_bytes(value.split(|byte| *byte == b' ').next().unwrap_or_default())
563                .unwrap_or(TrustLevel::Undefined);
564        } else if let Some(value) = line.strip_prefix(b"VALIDSIG ") {
565            let fields: Vec<_> = value.split(|byte| *byte == b' ').collect();
566            outcome.fingerprint = fields
567                .first()
568                .filter(|value| !value.is_empty())
569                .map(|value| BString::from(*value));
570            outcome.primary_key_fingerprint = fields
571                .get(9)
572                .filter(|value| !value.is_empty())
573                .map(|value| BString::from(*value));
574        }
575    }
576    outcome
577}
578
579fn parse_ssh_output(output: BString, signer: Option<BString>, trust_level: TrustLevel) -> Outcome {
580    let status = if output.starts_with(b"Good \"git\" signature") {
581        Status::Good
582    } else {
583        Status::Bad
584    };
585    let fingerprint = output
586        .lines()
587        .next()
588        .and_then(|line| line.rsplit_once_str(" key "))
589        .map(|(_, value)| value.into());
590    Outcome {
591        format: Format::Ssh,
592        status,
593        trust_level,
594        signer,
595        key: fingerprint.clone(),
596        fingerprint,
597        primary_key_fingerprint: None,
598        raw_output: output.clone(),
599        output,
600        valid: false,
601    }
602}
603
604#[cfg(test)]
605mod tests {
606    use super::*;
607
608    #[test]
609    fn parses_gpg_status() {
610        let Outcome {
611            format: _,
612            status,
613            trust_level,
614            signer,
615            key: _,
616            fingerprint,
617            primary_key_fingerprint,
618            output: _,
619            raw_output: _,
620            valid: _,
621        } = parse_gpg_output(
622            Format::OpenPgp,
623            "Good signature".into(),
624            "[GNUPG:] GOODSIG 0123456789ABCDEF Fixture Signer\n\
625             [GNUPG:] VALIDSIG FINGERPRINT 0 0 0 0 0 0 0 0 PRIMARY\n\
626             [GNUPG:] TRUST_FULLY 0 pgp\n"
627                .into(),
628        );
629        assert_eq!(status, Status::Good, "the signature is good");
630        assert_eq!(trust_level, TrustLevel::Fully, "the key is fully trusted");
631        assert_eq!(
632            signer.as_ref().map(|value| value.as_slice()),
633            Some(b"Fixture Signer".as_slice()),
634            "the signer identity is parsed"
635        );
636        assert_eq!(
637            fingerprint.as_ref().map(|value| value.as_slice()),
638            Some(b"FINGERPRINT".as_slice()),
639            "the signing-key fingerprint is parsed"
640        );
641        assert_eq!(
642            primary_key_fingerprint.as_ref().map(|value| value.as_slice()),
643            Some(b"PRIMARY".as_slice()),
644            "the primary-key fingerprint is parsed"
645        );
646    }
647
648    #[test]
649    fn parses_ssh_output() {
650        let Outcome {
651            format,
652            status,
653            trust_level,
654            signer,
655            key,
656            fingerprint,
657            primary_key_fingerprint,
658            output,
659            raw_output,
660            valid,
661        } = parse_ssh_output(
662            "Good \"git\" signature for Fixture Signer with ED25519 key SHA256:fixture\n".into(),
663            Some("Fixture Signer".into()),
664            TrustLevel::Fully,
665        );
666        assert_eq!(format, Format::Ssh, "the signature format is SSH");
667        assert_eq!(status, Status::Good, "Git's success output is recognized");
668        assert_eq!(trust_level, TrustLevel::Fully, "the supplied trust level is retained");
669        assert_eq!(signer, Some("Fixture Signer".into()), "the supplied signer is retained");
670        assert_eq!(key, Some("SHA256:fixture".into()), "the key fingerprint is parsed");
671        assert_eq!(fingerprint, key, "the key and fingerprint are identical for SSH");
672        assert_eq!(primary_key_fingerprint, None, "SSH has no primary-key fingerprint");
673        assert_eq!(output, raw_output, "SSH has no separate machine-readable output");
674        assert!(!valid, "parsing alone does not establish validity");
675
676        assert_eq!(
677            parse_ssh_output("invalid signature".into(), None, TrustLevel::Undefined).status,
678            Status::Bad,
679            "all output not starting with Git's success marker is bad"
680        );
681    }
682
683    #[test]
684    fn rejects_good_ssh_output_from_a_failed_verifier() -> Result<(), Error> {
685        let signed = SignedData::new(b"payloadsignature", 7..16);
686        let outcome = signed.verify(
687            BStr::new(b"-----BEGIN SSH SIGNATURE-----\n"),
688            Options::Ssh {
689                program: r#"if [ "$2" = find-principals ]; then printf 'fixture\n'; else printf 'Good "git" signature for fixture with ED25519 key SHA256:fixture\n'; exit 1; fi # "$@""#.into(),
690                program_arguments: Vec::new(),
691                environment: Vec::new(),
692                allowed_signers: "unused".into(),
693                revocation_file: None,
694                verify_time: gix_date::Time::default(),
695                minimum_trust: TrustLevel::Undefined,
696            },
697        )?;
698        assert_eq!(outcome.status, Status::Good, "the verifier's text is retained");
699        assert!(!outcome.is_valid(), "a failed verifier cannot produce a valid outcome");
700        Ok(())
701    }
702
703    #[test]
704    fn parses_signature_formats_and_trust_levels() {
705        for (signature, expected) in [
706            (b"-----BEGIN PGP SIGNATURE-----".as_slice(), Format::OpenPgp),
707            (b"-----BEGIN PGP MESSAGE-----".as_slice(), Format::OpenPgp),
708            (b"-----BEGIN SIGNED MESSAGE-----".as_slice(), Format::X509),
709            (b"-----BEGIN SSH SIGNATURE-----".as_slice(), Format::Ssh),
710        ] {
711            assert_eq!(Format::from_signature(signature), Some(expected));
712        }
713        assert_eq!(Format::from_signature(b"not a signature"), None);
714
715        for (name, expected) in [
716            (b"undefined".as_slice(), TrustLevel::Undefined),
717            (b"NEVER".as_slice(), TrustLevel::Never),
718            (b"Marginal".as_slice(), TrustLevel::Marginal),
719            (b"fully".as_slice(), TrustLevel::Fully),
720            (b"ultimate".as_slice(), TrustLevel::Ultimate),
721        ] {
722            assert_eq!(TrustLevel::from_bytes(name), Some(expected));
723        }
724        assert_eq!(TrustLevel::from_bytes(b"unknown"), None);
725    }
726
727    #[test]
728    fn rejects_unsupported_and_mismatched_formats_before_running_a_program() {
729        let signed = SignedData::new(b"payloadsignature", 7..16);
730        let options = Options::X509 {
731            program: "must-not-run".into(),
732            program_arguments: Vec::new(),
733            environment: Vec::new(),
734            minimum_trust: TrustLevel::Undefined,
735        };
736        assert!(matches!(
737            signed.verify(BStr::new(b"not a signature"), options.clone()),
738            Err(Error::UnsupportedFormat)
739        ));
740        assert!(
741            matches!(
742                signed.verify(BStr::new(b"-----BEGIN SSH SIGNATURE-----\n"), options),
743                Err(Error::FormatMismatch {
744                    program_format: Format::X509,
745                    signature_format: Format::Ssh,
746                })
747            ),
748            "the mismatch identifies both the configured program and detected signature formats"
749        );
750    }
751}