1use std::ffi::OsString;
22use std::io::Write;
23use std::path::{Path, PathBuf};
24use std::sync::Arc;
25
26use crate::command_runner::{
27 system_command_runner, CommandEnvironment, CommandOutput, CommandRunner, CommandSpec,
28 CommandStdin, CommandStdio,
29};
30use crate::config::ConfigSet;
31use crate::error::{Error, Result};
32use crate::objects::HashAlgo;
33
34pub const GPG_SIG_HEADER_SHA1: &str = "gpgsig";
36pub const GPG_SIG_HEADER_SHA256: &str = "gpgsig-sha256";
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
43pub enum TrustLevel {
44 #[default]
45 Undefined = 0,
47 Never = 1,
49 Marginal = 2,
51 Fully = 3,
53 Ultimate = 4,
55}
56
57impl TrustLevel {
58 pub fn display_key(self) -> &'static str {
60 match self {
61 TrustLevel::Undefined => "undefined",
62 TrustLevel::Never => "never",
63 TrustLevel::Marginal => "marginal",
64 TrustLevel::Fully => "fully",
65 TrustLevel::Ultimate => "ultimate",
66 }
67 }
68
69 fn from_status(level: &str) -> Option<TrustLevel> {
71 match level {
72 "UNDEFINED" => Some(TrustLevel::Undefined),
73 "NEVER" => Some(TrustLevel::Never),
74 "MARGINAL" => Some(TrustLevel::Marginal),
75 "FULLY" => Some(TrustLevel::Fully),
76 "ULTIMATE" => Some(TrustLevel::Ultimate),
77 _ => None,
78 }
79 }
80
81 pub fn from_config(value: &str) -> Option<TrustLevel> {
83 TrustLevel::from_status(&value.to_ascii_uppercase())
84 }
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum GpgFormat {
90 OpenPgp,
92 X509,
94 Ssh,
96}
97
98impl GpgFormat {
99 pub fn from_name(name: &str) -> Option<GpgFormat> {
103 match name {
104 "openpgp" => Some(GpgFormat::OpenPgp),
105 "x509" => Some(GpgFormat::X509),
106 "ssh" => Some(GpgFormat::Ssh),
107 _ => None,
108 }
109 }
110
111 fn name(self) -> &'static str {
113 match self {
114 GpgFormat::OpenPgp => "openpgp",
115 GpgFormat::X509 => "x509",
116 GpgFormat::Ssh => "ssh",
117 }
118 }
119
120 fn default_program(self) -> &'static str {
122 match self {
123 GpgFormat::OpenPgp => "gpg",
124 GpgFormat::X509 => "gpgsm",
125 GpgFormat::Ssh => "ssh-keygen",
126 }
127 }
128
129 pub fn from_signature(sig: &[u8]) -> Option<GpgFormat> {
133 const OPENPGP: &[&[u8]] = &[
134 b"-----BEGIN PGP SIGNATURE-----",
135 b"-----BEGIN PGP MESSAGE-----",
136 ];
137 const X509: &[&[u8]] = &[b"-----BEGIN SIGNED MESSAGE-----"];
138 const SSH: &[&[u8]] = &[b"-----BEGIN SSH SIGNATURE-----"];
139 for prefix in OPENPGP {
140 if sig.starts_with(prefix) {
141 return Some(GpgFormat::OpenPgp);
142 }
143 }
144 for prefix in X509 {
145 if sig.starts_with(prefix) {
146 return Some(GpgFormat::X509);
147 }
148 }
149 for prefix in SSH {
150 if sig.starts_with(prefix) {
151 return Some(GpgFormat::Ssh);
152 }
153 }
154 None
155 }
156
157 fn verify_args(self) -> &'static [&'static str] {
159 match self {
160 GpgFormat::OpenPgp => &["--keyid-format=long"],
161 GpgFormat::X509 => &["--keyid-format=long"],
162 GpgFormat::Ssh => &[],
163 }
164 }
165}
166
167#[derive(Clone)]
169pub struct GpgConfig {
170 pub format: GpgFormat,
172 pub program: String,
175 pub generic_program: Option<String>,
177 pub openpgp_program: Option<String>,
179 pub x509_program: Option<String>,
181 pub ssh_program: Option<String>,
183 pub signing_key: Option<String>,
185 pub min_trust_level: Option<TrustLevel>,
187 pub ssh_allowed_signers: Option<String>,
189 pub ssh_revocation_file: Option<String>,
191 pub command_runner: Arc<dyn CommandRunner>,
193}
194
195impl std::fmt::Debug for GpgConfig {
196 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197 f.debug_struct("GpgConfig")
198 .field("format", &self.format)
199 .field("program", &self.program)
200 .finish_non_exhaustive()
201 }
202}
203
204fn signing_spawn(
205 cfg: &GpgConfig,
206 spec: CommandSpec,
207 payload: Option<&[u8]>,
208) -> Result<CommandOutput> {
209 let mut running = cfg
210 .command_runner
211 .spawn(&spec)
212 .map_err(|e| Error::Signing(format!("could not run signing program: {e}")))?;
213 if let Some(data) = payload {
214 let Some(stdin) = running.stdin_mut() else {
215 return Err(Error::Signing(
216 "could not write payload to signing program stdin".into(),
217 ));
218 };
219 stdin.write_all(data).map_err(|e| {
220 Error::Signing(format!("could not write payload to signing program: {e}"))
221 })?;
222 }
223 running
224 .wait_with_output()
225 .map_err(|e| Error::Signing(format!("failed waiting for signing program: {e}")))
226}
227
228fn stdin_pipe(data: &[u8]) -> CommandStdin {
229 CommandStdin::Pipe(data.to_vec())
230}
231
232impl GpgConfig {
233 pub fn from_config(config: &ConfigSet) -> Result<GpgConfig> {
240 let format = match config.get("gpg.format") {
241 Some(raw) => GpgFormat::from_name(&raw).ok_or_else(|| {
242 Error::Config(format!("invalid value for 'gpg.format': '{raw}'").into())
243 })?,
244 None => GpgFormat::OpenPgp,
245 };
246
247 let nonempty = |k: &str| config.get(k).filter(|p| !p.is_empty());
248 let generic_program = nonempty("gpg.program");
249 let fmt_program = |f: GpgFormat| nonempty(&format!("gpg.{}.program", f.name()));
250 let openpgp_program = fmt_program(GpgFormat::OpenPgp);
251 let x509_program = fmt_program(GpgFormat::X509);
252 let ssh_program = fmt_program(GpgFormat::Ssh);
253
254 let program = resolve_program_for_format(
256 format,
257 generic_program.as_deref(),
258 match format {
259 GpgFormat::OpenPgp => openpgp_program.as_deref(),
260 GpgFormat::X509 => x509_program.as_deref(),
261 GpgFormat::Ssh => ssh_program.as_deref(),
262 },
263 );
264
265 let signing_key = config.get("user.signingkey").filter(|k| !k.is_empty());
266
267 let min_trust_level = config
268 .get("gpg.mintrustlevel")
269 .and_then(|v| TrustLevel::from_config(&v));
270
271 let ssh_allowed_signers = config
274 .get("gpg.ssh.allowedsignersfile")
275 .filter(|p| !p.is_empty())
276 .map(|p| expand_tilde(&p));
277 let ssh_revocation_file = config
278 .get("gpg.ssh.revocationfile")
279 .filter(|p| !p.is_empty())
280 .map(|p| expand_tilde(&p));
281
282 Ok(GpgConfig {
283 format,
284 program,
285 generic_program,
286 openpgp_program,
287 x509_program,
288 ssh_program,
289 signing_key,
290 min_trust_level,
291 ssh_allowed_signers,
292 ssh_revocation_file,
293 command_runner: system_command_runner(),
294 })
295 }
296
297 #[must_use]
299 pub fn with_command_runner(mut self, runner: Arc<dyn CommandRunner>) -> Self {
300 self.command_runner = runner;
301 self
302 }
303
304 fn program_for(&self, format: GpgFormat) -> String {
309 let fmt_program = match format {
310 GpgFormat::OpenPgp => self.openpgp_program.as_deref(),
311 GpgFormat::X509 => self.x509_program.as_deref(),
312 GpgFormat::Ssh => self.ssh_program.as_deref(),
313 };
314 resolve_program_for_format(format, self.generic_program.as_deref(), fmt_program)
315 }
316
317 pub fn resolve_signing_key(
321 &self,
322 key_override: Option<&str>,
323 committer_default: &str,
324 ) -> String {
325 if let Some(k) = key_override {
326 if !k.is_empty() {
327 return k.to_owned();
328 }
329 }
330 if let Some(k) = &self.signing_key {
331 return k.clone();
332 }
333 committer_default.to_owned()
334 }
335
336 pub fn resolve_program_path(&self) -> Result<PathBuf> {
341 resolve_program(&self.program)
342 }
343}
344
345fn resolve_program_for_format(
348 format: GpgFormat,
349 generic_program: Option<&str>,
350 fmt_program: Option<&str>,
351) -> String {
352 fmt_program
353 .or(generic_program)
354 .filter(|p| !p.is_empty())
355 .map(|p| p.to_owned())
356 .unwrap_or_else(|| format.default_program().to_owned())
357}
358
359fn resolve_program(program: &str) -> Result<PathBuf> {
361 if program == "~" {
363 if let Some(home) = home_dir() {
364 return Ok(home);
365 }
366 }
367 if let Some(rest) = program.strip_prefix("~/") {
368 if let Some(home) = home_dir() {
369 return Ok(home.join(rest));
370 }
371 }
372
373 let path = Path::new(program);
374 if path.is_absolute() || program.contains('/') {
376 return Ok(path.to_path_buf());
377 }
378
379 if let Some(found) = search_path(program) {
381 return Ok(found);
382 }
383
384 Ok(path.to_path_buf())
387}
388
389fn search_path(name: &str) -> Option<PathBuf> {
391 let paths = crate::environment::Environment::empty().var_os("PATH")?;
392 for dir in paths.to_string_lossy().split(':').map(PathBuf::from) {
393 if dir.as_os_str().is_empty() {
394 continue;
395 }
396 let candidate = dir.join(name);
397 if is_executable_file(&candidate) {
398 return Some(candidate);
399 }
400 }
401 None
402}
403
404#[cfg(unix)]
405fn is_executable_file(path: &Path) -> bool {
406 use std::os::unix::fs::PermissionsExt;
407 match std::fs::metadata(path) {
408 Ok(meta) => meta.is_file() && (meta.permissions().mode() & 0o111 != 0),
409 Err(_) => false,
410 }
411}
412
413#[cfg(not(unix))]
414fn is_executable_file(path: &Path) -> bool {
415 path.is_file()
416}
417
418fn home_dir() -> Option<PathBuf> {
420 crate::environment::Environment::empty()
421 .var_os("HOME")
422 .map(PathBuf::from)
423}
424
425fn expand_tilde(path: &str) -> String {
428 if path == "~" {
429 if let Some(home) = home_dir() {
430 return home.to_string_lossy().into_owned();
431 }
432 }
433 if let Some(rest) = path.strip_prefix("~/") {
434 if let Some(home) = home_dir() {
435 return home.join(rest).to_string_lossy().into_owned();
436 }
437 }
438 path.to_owned()
439}
440
441pub fn should_sign_commit(config: &ConfigSet, sign_override: Option<bool>) -> Result<bool> {
450 if let Some(force) = sign_override {
451 return Ok(force);
452 }
453 match config
454 .get_bool("commit.gpgsign")
455 .or_else(|| config.get_bool("commit.gpgSign"))
456 {
457 Some(Ok(v)) => Ok(v),
458 Some(Err(e)) => Err(Error::Config(format!("commit.gpgsign: {e}").into())),
459 None => Ok(false),
460 }
461}
462
463pub fn should_sign_tag(config: &ConfigSet, sign_override: Option<bool>) -> Result<bool> {
472 if let Some(force) = sign_override {
473 return Ok(force);
474 }
475 match config
476 .get_bool("tag.gpgSign")
477 .or_else(|| config.get_bool("tag.gpgsign"))
478 {
479 Some(Ok(v)) => Ok(v),
480 Some(Err(e)) => Err(Error::Config(format!("tag.gpgSign: {e}").into())),
481 None => Ok(false),
482 }
483}
484
485#[must_use]
487pub fn commit_gpg_sig_header(hash_algo: HashAlgo) -> &'static str {
488 match hash_algo {
489 HashAlgo::Sha256 => GPG_SIG_HEADER_SHA256,
490 HashAlgo::Sha1 => GPG_SIG_HEADER_SHA1,
491 }
492}
493
494pub fn sign_serialized_commit(
507 cfg: &GpgConfig,
508 unsigned_bytes: &[u8],
509 committer_ident: &str,
510 signing_key_override: Option<&str>,
511 hash_algo: HashAlgo,
512) -> Result<Vec<u8>> {
513 let committer_key = committer_signing_default(committer_ident);
514 let signing_key = cfg.resolve_signing_key(signing_key_override, &committer_key);
515 let sig = sign_buffer(cfg, unsigned_bytes, &signing_key)?;
516 let header = commit_gpg_sig_header(hash_algo);
517 Ok(add_header_signature(unsigned_bytes, &sig, header))
518}
519
520#[must_use]
525pub fn append_tag_signature(unsigned_bytes: &[u8], sig: &[u8]) -> Vec<u8> {
526 let mut out = Vec::with_capacity(unsigned_bytes.len() + sig.len());
527 out.extend_from_slice(unsigned_bytes);
528 out.extend_from_slice(sig);
529 out
530}
531
532pub fn sign_serialized_tag(
542 cfg: &GpgConfig,
543 unsigned_bytes: &[u8],
544 tagger_ident: &str,
545 signing_key_override: Option<&str>,
546) -> Result<Vec<u8>> {
547 let tagger_key = committer_signing_default(tagger_ident);
548 let signing_key = cfg.resolve_signing_key(signing_key_override, &tagger_key);
549 let sig = sign_buffer(cfg, unsigned_bytes, &signing_key)?;
550 Ok(append_tag_signature(unsigned_bytes, &sig))
551}
552
553pub fn sign_buffer(cfg: &GpgConfig, payload: &[u8], signing_key: &str) -> Result<Vec<u8>> {
566 if cfg.format == GpgFormat::Ssh {
567 return sign_buffer_ssh(cfg, payload, signing_key);
568 }
569
570 let program = cfg.resolve_program_path()?;
571
572 let spec = CommandSpec {
573 program: program.clone(),
574 args: vec![
575 OsString::from("--status-fd=2"),
576 OsString::from("-bsau"),
577 OsString::from(signing_key),
578 ],
579 shell: None,
580 cwd: None,
581 env: CommandEnvironment::inherit_process_only(),
582 stdin: stdin_pipe(payload),
583 stdout: CommandStdio::Pipe,
584 stderr: CommandStdio::Pipe,
585 };
586 let output = signing_spawn(cfg, spec, None)?;
587
588 let status_text = String::from_utf8_lossy(&output.stderr);
589
590 if !output.status.success || !has_sig_created(&status_text) {
591 let detail = if status_text.trim().is_empty() {
592 "(no gpg output)".to_owned()
593 } else {
594 status_text.into_owned()
595 };
596 return Err(Error::Signing(format!(
597 "gpg failed to sign the data:\n{detail}"
598 )));
599 }
600
601 Ok(output.stdout)
602}
603
604fn has_sig_created(status: &str) -> bool {
607 status
608 .lines()
609 .any(|line| line.starts_with("[GNUPG:] SIG_CREATED "))
610}
611
612fn is_literal_ssh_key(s: &str) -> Option<&str> {
617 if let Some(rest) = s.strip_prefix("key::") {
618 return Some(rest);
619 }
620 if s.starts_with("ssh-") {
621 return Some(s);
622 }
623 None
624}
625
626fn sign_buffer_ssh(cfg: &GpgConfig, payload: &[u8], signing_key: &str) -> Result<Vec<u8>> {
638 if signing_key.is_empty() {
639 return Err(Error::Signing(
640 "user.signingKey needs to be set for ssh signing".to_owned(),
641 ));
642 }
643
644 let program = cfg.resolve_program_path()?;
645
646 let mut literal_key_tmp: Option<PathBuf> = None;
649 let (key_file, literal): (String, bool) = match is_literal_ssh_key(signing_key) {
650 Some(literal_key) => {
651 let path = write_temp_file_named(literal_key.as_bytes(), "git_signing_key")?;
652 let p = path.to_string_lossy().into_owned();
653 literal_key_tmp = Some(path);
654 (p, true)
655 }
656 None => (expand_tilde(signing_key), false),
657 };
658
659 let buffer_path = match write_temp_file_named(payload, "git_signing_buffer") {
662 Ok(p) => p,
663 Err(e) => {
664 if let Some(p) = &literal_key_tmp {
665 let _ = std::fs::remove_file(p);
666 }
667 return Err(e);
668 }
669 };
670
671 let mut args = vec![
672 OsString::from("-Y"),
673 OsString::from("sign"),
674 OsString::from("-n"),
675 OsString::from("git"),
676 OsString::from("-f"),
677 OsString::from(key_file.as_str()),
678 ];
679 if literal {
680 args.push(OsString::from("-U"));
681 }
682 args.push(OsString::from(buffer_path.to_string_lossy().as_ref()));
683 let spec = CommandSpec {
684 program: program.clone(),
685 args,
686 shell: None,
687 cwd: None,
688 env: CommandEnvironment::inherit_process_only(),
689 stdin: CommandStdin::Null,
690 stdout: CommandStdio::Pipe,
691 stderr: CommandStdio::Pipe,
692 };
693
694 let cleanup = |literal_key_tmp: &Option<PathBuf>, buffer_path: &Path| {
695 if let Some(p) = literal_key_tmp {
696 let _ = std::fs::remove_file(p);
697 }
698 let _ = std::fs::remove_file(buffer_path);
699 let _ = std::fs::remove_file(sig_sibling(buffer_path));
700 };
701
702 let output = match signing_spawn(cfg, spec, None) {
703 Ok(o) => o,
704 Err(e) => {
705 cleanup(&literal_key_tmp, &buffer_path);
706 return Err(e);
707 }
708 };
709
710 if !output.status.success {
711 let stderr = String::from_utf8_lossy(&output.stderr);
712 cleanup(&literal_key_tmp, &buffer_path);
713 if stderr.contains("usage:") {
714 return Err(Error::Signing(
715 "ssh-keygen -Y sign is needed for ssh signing (available in openssh version 8.2p1+)"
716 .to_owned(),
717 ));
718 }
719 return Err(Error::Signing(stderr.into_owned()));
720 }
721
722 let sig_path = sig_sibling(&buffer_path);
723 let result = std::fs::read(&sig_path).map_err(|e| {
724 Error::Signing(format!(
725 "failed reading ssh signing data buffer from '{}': {e}",
726 sig_path.display()
727 ))
728 });
729 cleanup(&literal_key_tmp, &buffer_path);
730
731 let mut sig = result?;
732 strip_cr(&mut sig);
735 Ok(sig)
736}
737
738fn sig_sibling(buffer_path: &Path) -> PathBuf {
740 let mut s = buffer_path.as_os_str().to_owned();
741 s.push(".sig");
742 PathBuf::from(s)
743}
744
745fn strip_cr(buf: &mut Vec<u8>) {
747 let mut out = Vec::with_capacity(buf.len());
748 let mut i = 0;
749 while i < buf.len() {
750 if buf[i] == b'\r' && buf.get(i + 1) == Some(&b'\n') {
751 i += 1;
752 continue;
753 }
754 out.push(buf[i]);
755 i += 1;
756 }
757 *buf = out;
758}
759
760pub fn add_header_signature(buf: &[u8], sig: &[u8], header: &str) -> Vec<u8> {
767 let inspos = find_double_newline(buf).map(|p| p + 1).unwrap_or(buf.len());
770
771 let mut out = Vec::with_capacity(buf.len() + sig.len() + header.len() + 16);
772 out.extend_from_slice(&buf[..inspos]);
773
774 let mut first = true;
775 let mut copypos = 0usize;
776 while copypos < sig.len() {
777 let bol = copypos;
778 let end = match memchr(sig, copypos, b'\n') {
780 Some(idx) => idx + 1,
781 None => sig.len(),
782 };
783
784 if first {
785 out.extend_from_slice(header.as_bytes());
786 first = false;
787 }
788 out.push(b' ');
789 out.extend_from_slice(&sig[bol..end]);
790 copypos = end;
791 }
792
793 out.extend_from_slice(&buf[inspos..]);
794 out
795}
796
797fn find_double_newline(buf: &[u8]) -> Option<usize> {
799 let mut i = 0;
800 while i + 1 < buf.len() {
801 if buf[i] == b'\n' && buf[i + 1] == b'\n' {
802 return Some(i);
803 }
804 i += 1;
805 }
806 None
807}
808
809fn memchr(buf: &[u8], from: usize, needle: u8) -> Option<usize> {
811 buf.get(from..)
812 .and_then(|s| s.iter().position(|&b| b == needle))
813 .map(|p| p + from)
814}
815
816#[derive(Debug, Clone, Default)]
818pub struct SignatureCheck {
819 pub signature: Vec<u8>,
821 pub payload: Vec<u8>,
823 pub result: char,
826 pub trust_level: TrustLevel,
828 pub key: Option<String>,
830 pub signer: Option<String>,
832 pub fingerprint: Option<String>,
834 pub primary_key_fingerprint: Option<String>,
836 pub output: String,
838 pub gpg_status: String,
840 pub verifier_failed: bool,
845}
846
847impl SignatureCheck {
848 pub fn default_none() -> SignatureCheck {
850 SignatureCheck {
851 result: 'N',
852 trust_level: TrustLevel::Undefined,
853 ..Default::default()
854 }
855 }
856
857 pub fn is_good(&self) -> bool {
860 self.result == 'G' || self.result == 'Y'
861 }
862
863 pub fn verify_status(&self, min_trust_level: Option<TrustLevel>) -> bool {
866 if self.verifier_failed {
867 return false;
868 }
869 if !self.is_good() {
870 return false;
871 }
872 if let Some(min) = min_trust_level {
873 if self.trust_level < min {
874 return false;
875 }
876 }
877 true
878 }
879}
880
881pub fn extract_signed_payload(raw_commit: &[u8]) -> Option<(Vec<u8>, Vec<u8>)> {
889 let header_end = find_double_newline(raw_commit)?;
891 let header = &raw_commit[..=header_end]; let body = &raw_commit[header_end + 1..]; let mut payload = Vec::with_capacity(raw_commit.len());
895 let mut signature = Vec::new();
896 let mut found = false;
897
898 let mut idx = 0;
899 while idx < header.len() {
900 let line_end = memchr(header, idx, b'\n')
901 .map(|p| p + 1)
902 .unwrap_or(header.len());
903 let line = &header[idx..line_end];
904
905 let Some((_, prefix_len)) = gpg_sig_header_line(line) else {
906 payload.extend_from_slice(line);
907 idx = line_end;
908 continue;
909 };
910
911 if !found {
912 found = true;
913 signature.extend_from_slice(&line[prefix_len..]);
914 idx = line_end;
915 while idx < header.len() {
917 let cont_end = memchr(header, idx, b'\n')
918 .map(|p| p + 1)
919 .unwrap_or(header.len());
920 let cont = &header[idx..cont_end];
921 if cont.first() == Some(&b' ') {
922 signature.extend_from_slice(&cont[1..]);
923 idx = cont_end;
924 } else {
925 break;
926 }
927 }
928 continue;
929 }
930
931 payload.extend_from_slice(line);
932 idx = line_end;
933 }
934
935 if !found {
936 return None;
937 }
938
939 payload.extend_from_slice(body);
940 Some((payload, signature))
941}
942
943#[must_use]
948pub fn extra_headers_without_commit_signatures(extra: &[u8]) -> Vec<u8> {
949 let mut out = Vec::with_capacity(extra.len());
950 let mut idx = 0;
951 while idx < extra.len() {
952 let line_end = memchr(extra, idx, b'\n')
953 .map(|p| p + 1)
954 .unwrap_or(extra.len());
955 let line = &extra[idx..line_end];
956
957 if gpg_sig_header_line(line).is_some() {
958 idx = line_end;
959 while idx < extra.len() {
960 let cont_end = memchr(extra, idx, b'\n')
961 .map(|p| p + 1)
962 .unwrap_or(extra.len());
963 let cont = &extra[idx..cont_end];
964 if cont.first() == Some(&b' ') {
965 idx = cont_end;
966 } else {
967 break;
968 }
969 }
970 continue;
971 }
972
973 out.extend_from_slice(line);
974 idx = line_end;
975 }
976 out
977}
978
979fn gpg_sig_header_line(line: &[u8]) -> Option<(&'static str, usize)> {
982 for header in [GPG_SIG_HEADER_SHA256, GPG_SIG_HEADER_SHA1] {
983 if line.starts_with(header.as_bytes()) && line.get(header.len()) == Some(&b' ') {
984 return Some((header, header.len() + 1));
985 }
986 }
987 None
988}
989
990pub fn verify_commit(cfg: &GpgConfig, raw_commit: &[u8]) -> Result<SignatureCheck> {
997 let (payload, signature) = match extract_signed_payload(raw_commit) {
998 Some(parts) => parts,
999 None => return Ok(SignatureCheck::default_none()),
1000 };
1001
1002 let detected_format = GpgFormat::from_signature(&signature).unwrap_or(cfg.format);
1006
1007 if detected_format == GpgFormat::Ssh {
1008 return verify_ssh_signed_buffer(cfg, payload, signature);
1009 }
1010
1011 let program = resolve_program(&cfg.program_for(detected_format))?;
1012
1013 let sig_path = write_temp_file(&signature)?;
1015
1016 let mut args = vec![OsString::from("--status-fd=1")];
1017 for a in detected_format.verify_args() {
1018 args.push(OsString::from(a));
1019 }
1020 args.push(OsString::from("--verify"));
1021 args.push(OsString::from(sig_path.to_string_lossy().as_ref()));
1022 args.push(OsString::from("-"));
1023 let spec = CommandSpec {
1024 program: program.clone(),
1025 args,
1026 shell: None,
1027 cwd: None,
1028 env: CommandEnvironment::inherit_process_only(),
1029 stdin: stdin_pipe(&payload),
1030 stdout: CommandStdio::Pipe,
1031 stderr: CommandStdio::Pipe,
1032 };
1033
1034 let output = match signing_spawn(cfg, spec, None) {
1035 Ok(o) => o,
1036 Err(e) => {
1037 let _ = std::fs::remove_file(&sig_path);
1038 return Err(e);
1039 }
1040 };
1041 let _ = std::fs::remove_file(&sig_path);
1042
1043 let gpg_status = String::from_utf8_lossy(&output.stdout).into_owned();
1045 let human = String::from_utf8_lossy(&output.stderr).into_owned();
1046
1047 let mut sigc = SignatureCheck {
1048 signature,
1049 payload,
1050 result: 'N',
1051 trust_level: TrustLevel::Undefined,
1052 gpg_status: gpg_status.clone(),
1053 output: human,
1054 ..Default::default()
1055 };
1056
1057 parse_gpg_output(&mut sigc, &gpg_status);
1058
1059 Ok(sigc)
1060}
1061
1062fn parse_ssh_output(sigc: &mut SignatureCheck) {
1075 sigc.result = 'B';
1076 sigc.trust_level = TrustLevel::Never;
1077 sigc.key = None;
1078 sigc.signer = None;
1079 sigc.fingerprint = None;
1080 sigc.primary_key_fingerprint = None;
1081
1082 let first_line = sigc.output.split('\n').next().unwrap_or("");
1083
1084 let after_key;
1085 if let Some(rest) = first_line.strip_prefix("Good \"git\" signature for ") {
1086 match rest.rfind(" with ") {
1089 Some(idx) => {
1090 let principal = &rest[..idx];
1091 if principal.is_empty() {
1092 return;
1093 }
1094 sigc.result = 'G';
1095 sigc.trust_level = TrustLevel::Fully;
1096 sigc.signer = Some(principal.to_owned());
1097 after_key = &rest[idx + " with ".len()..];
1098 }
1099 None => return,
1100 }
1101 } else if let Some(rest) = first_line.strip_prefix("Good \"git\" signature with ") {
1102 sigc.result = 'G';
1103 sigc.trust_level = TrustLevel::Undefined;
1104 after_key = rest;
1105 } else {
1106 return;
1107 }
1108
1109 match after_key.find("key ") {
1111 Some(pos) => {
1112 let fpr = after_key[pos + "key ".len()..].to_owned();
1113 sigc.fingerprint = Some(fpr.clone());
1114 sigc.key = Some(fpr);
1115 }
1116 None => {
1117 sigc.result = 'B';
1119 }
1120 }
1121}
1122
1123fn payload_committer_timestamp(payload: &[u8]) -> Option<u64> {
1126 let ident_line =
1127 find_header_line(payload, b"committer").or_else(|| find_header_line(payload, b"tagger"))?;
1128 let line = std::str::from_utf8(ident_line).ok()?;
1129 let mut it = line.split_whitespace().rev();
1132 let _tz = it.next()?;
1133 let ts = it.next()?;
1134 ts.parse::<u64>().ok()
1135}
1136
1137fn find_header_line<'a>(payload: &'a [u8], name: &[u8]) -> Option<&'a [u8]> {
1140 let mut idx = 0;
1141 while idx < payload.len() {
1142 let line_end = memchr(payload, idx, b'\n').unwrap_or(payload.len());
1143 let line = &payload[idx..line_end];
1144 if line.is_empty() {
1145 return None;
1147 }
1148 if line.len() > name.len() + 1 && &line[..name.len()] == name && line[name.len()] == b' ' {
1149 return Some(&line[name.len() + 1..]);
1150 }
1151 idx = line_end + 1;
1152 }
1153 None
1154}
1155
1156fn verify_time_arg(timestamp: u64) -> String {
1159 use crate::git_date::show::{show_date, DateMode, DateModeType};
1160 let mut mode = DateMode {
1161 ty: DateModeType::Strftime,
1162 local: true,
1163 strftime_fmt: Some("%Y%m%d%H%M%S".to_owned()),
1164 };
1165 let formatted = show_date(timestamp, 0, &mut mode);
1166 format!("-Overify-time={formatted}")
1167}
1168
1169fn verify_ssh_signed_buffer(
1177 cfg: &GpgConfig,
1178 payload: Vec<u8>,
1179 signature: Vec<u8>,
1180) -> Result<SignatureCheck> {
1181 let mut sigc = SignatureCheck {
1182 signature: signature.clone(),
1183 payload: payload.clone(),
1184 result: 'N',
1185 trust_level: TrustLevel::Undefined,
1186 ..Default::default()
1187 };
1188
1189 let allowed = match &cfg.ssh_allowed_signers {
1190 Some(a) if !a.is_empty() => a.clone(),
1191 _ => {
1192 sigc.result = 'B';
1193 sigc.trust_level = TrustLevel::Never;
1194 sigc.output = "gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification".to_owned();
1195 sigc.gpg_status = sigc.output.clone();
1196 return Ok(sigc);
1197 }
1198 };
1199
1200 let program = resolve_program(&cfg.program_for(GpgFormat::Ssh))?;
1204
1205 let sig_path = write_temp_file_named(&signature, "git_vtag")?;
1207
1208 let verify_time = payload_committer_timestamp(&payload).map(verify_time_arg);
1209
1210 let mut find_args = vec![
1212 OsString::from("-Y"),
1213 OsString::from("find-principals"),
1214 OsString::from("-f"),
1215 OsString::from(allowed.as_str()),
1216 OsString::from("-s"),
1217 OsString::from(sig_path.to_string_lossy().as_ref()),
1218 ];
1219 if let Some(vt) = &verify_time {
1220 find_args.push(OsString::from(vt.as_str()));
1221 }
1222 let find_spec = CommandSpec {
1223 program: program.clone(),
1224 args: find_args,
1225 shell: None,
1226 cwd: None,
1227 env: CommandEnvironment::inherit_process_only(),
1228 stdin: CommandStdin::Null,
1229 stdout: CommandStdio::Pipe,
1230 stderr: CommandStdio::Pipe,
1231 };
1232 let find_out = signing_spawn(cfg, find_spec, None).inspect_err(|_| {
1233 let _ = std::fs::remove_file(&sig_path);
1234 })?;
1235
1236 let find_stdout = String::from_utf8_lossy(&find_out.stdout).into_owned();
1237 let find_stderr = String::from_utf8_lossy(&find_out.stderr).into_owned();
1238
1239 if !find_out.status.success && find_stderr.contains("usage:") {
1240 let _ = std::fs::remove_file(&sig_path);
1241 return Err(Error::Signing(
1242 "ssh-keygen -Y find-principals/verify is needed for ssh signature verification (available in openssh version 8.2p1+)"
1243 .to_owned(),
1244 ));
1245 }
1246
1247 let mut verify_stdout = String::new();
1248 let mut verify_stderr = String::new();
1249 let mut verifier_failed;
1251
1252 if !find_out.status.success || find_stdout.trim().is_empty() {
1253 let mut check_args = vec![
1256 OsString::from("-Y"),
1257 OsString::from("check-novalidate"),
1258 OsString::from("-n"),
1259 OsString::from("git"),
1260 OsString::from("-s"),
1261 OsString::from(sig_path.to_string_lossy().as_ref()),
1262 ];
1263 if let Some(vt) = &verify_time {
1264 check_args.push(OsString::from(vt.as_str()));
1265 }
1266 let (out, err) = run_with_stdin(cfg, &program, check_args, &payload);
1267 verify_stdout = out;
1268 verify_stderr = err;
1269 verifier_failed = true;
1270 } else {
1271 verifier_failed = true;
1273 for principal in find_stdout.lines() {
1274 let principal = principal.trim_end_matches('\r');
1275 if principal.is_empty() {
1276 continue;
1277 }
1278 let mut verify_args = vec![
1279 OsString::from("-Y"),
1280 OsString::from("verify"),
1281 OsString::from("-n"),
1282 OsString::from("git"),
1283 OsString::from("-f"),
1284 OsString::from(allowed.as_str()),
1285 OsString::from("-I"),
1286 OsString::from(principal),
1287 OsString::from("-s"),
1288 OsString::from(sig_path.to_string_lossy().as_ref()),
1289 ];
1290 if let Some(vt) = &verify_time {
1291 verify_args.push(OsString::from(vt.as_str()));
1292 }
1293 if let Some(rev) = &cfg.ssh_revocation_file {
1294 if Path::new(rev).exists() {
1295 verify_args.push(OsString::from("-r"));
1296 verify_args.push(OsString::from(rev.as_str()));
1297 }
1298 }
1299 let (out, err, ok) = run_with_stdin_status(cfg, &program, verify_args, &payload);
1300 verify_stdout = out;
1301 verify_stderr = err;
1302 verifier_failed = !(ok && verify_stdout.starts_with("Good"));
1304 if !verifier_failed {
1305 break;
1306 }
1307 }
1308 }
1309
1310 let _ = std::fs::remove_file(&sig_path);
1311
1312 let mut output = stripspace(&verify_stdout);
1319 let verify_stderr = stripspace(&verify_stderr);
1320 output.push_str(&find_stderr);
1321 output.push_str(&verify_stderr);
1322
1323 sigc.output = output;
1324 sigc.gpg_status = sigc.output.clone();
1325 parse_ssh_output(&mut sigc);
1326 sigc.verifier_failed = verifier_failed;
1329
1330 Ok(sigc)
1331}
1332
1333fn run_with_stdin(
1335 cfg: &GpgConfig,
1336 program: &Path,
1337 args: Vec<OsString>,
1338 input: &[u8],
1339) -> (String, String) {
1340 let (out, err, _ok) = run_with_stdin_status(cfg, program, args, input);
1341 (out, err)
1342}
1343
1344fn run_with_stdin_status(
1346 cfg: &GpgConfig,
1347 program: &Path,
1348 args: Vec<OsString>,
1349 input: &[u8],
1350) -> (String, String, bool) {
1351 let spec = CommandSpec {
1352 program: program.to_path_buf(),
1353 args,
1354 shell: None,
1355 cwd: None,
1356 env: CommandEnvironment::inherit_process_only(),
1357 stdin: stdin_pipe(input),
1358 stdout: CommandStdio::Pipe,
1359 stderr: CommandStdio::Pipe,
1360 };
1361 match signing_spawn(cfg, spec, None) {
1362 Ok(o) => (
1363 String::from_utf8_lossy(&o.stdout).into_owned(),
1364 String::from_utf8_lossy(&o.stderr).into_owned(),
1365 o.status.success,
1366 ),
1367 Err(_) => (String::new(), String::new(), false),
1368 }
1369}
1370
1371fn stripspace(s: &str) -> String {
1377 let mut out = String::with_capacity(s.len());
1378 let mut pending_empties = 0usize;
1379 let mut wrote_any = false;
1380 for line in s.split('\n') {
1381 let trimmed = line.trim_end_matches([' ', '\t', '\r']);
1382 if trimmed.is_empty() {
1383 pending_empties += 1;
1384 continue;
1385 }
1386 if pending_empties > 0 && wrote_any {
1387 out.push('\n');
1388 }
1389 pending_empties = 0;
1390 out.push_str(trimmed);
1391 out.push('\n');
1392 wrote_any = true;
1393 }
1394 out
1395}
1396
1397fn parse_gpg_output(sigc: &mut SignatureCheck, status: &str) {
1399 struct Entry {
1401 result: Option<char>,
1402 check: &'static str,
1403 exclusive: bool,
1404 keyid: bool,
1405 uid: bool,
1406 fingerprint: bool,
1407 trust: bool,
1408 }
1409 const TABLE: &[Entry] = &[
1410 Entry {
1411 result: Some('G'),
1412 check: "GOODSIG ",
1413 exclusive: true,
1414 keyid: true,
1415 uid: true,
1416 fingerprint: false,
1417 trust: false,
1418 },
1419 Entry {
1420 result: Some('B'),
1421 check: "BADSIG ",
1422 exclusive: true,
1423 keyid: true,
1424 uid: true,
1425 fingerprint: false,
1426 trust: false,
1427 },
1428 Entry {
1429 result: Some('E'),
1430 check: "ERRSIG ",
1431 exclusive: true,
1432 keyid: true,
1433 uid: false,
1434 fingerprint: false,
1435 trust: false,
1436 },
1437 Entry {
1438 result: Some('X'),
1439 check: "EXPSIG ",
1440 exclusive: true,
1441 keyid: true,
1442 uid: true,
1443 fingerprint: false,
1444 trust: false,
1445 },
1446 Entry {
1447 result: Some('Y'),
1448 check: "EXPKEYSIG ",
1449 exclusive: true,
1450 keyid: true,
1451 uid: true,
1452 fingerprint: false,
1453 trust: false,
1454 },
1455 Entry {
1456 result: Some('R'),
1457 check: "REVKEYSIG ",
1458 exclusive: true,
1459 keyid: true,
1460 uid: true,
1461 fingerprint: false,
1462 trust: false,
1463 },
1464 Entry {
1465 result: None,
1466 check: "VALIDSIG ",
1467 exclusive: false,
1468 keyid: false,
1469 uid: false,
1470 fingerprint: true,
1471 trust: false,
1472 },
1473 Entry {
1474 result: None,
1475 check: "TRUST_",
1476 exclusive: false,
1477 keyid: false,
1478 uid: false,
1479 fingerprint: false,
1480 trust: true,
1481 },
1482 ];
1483
1484 let mut seen_exclusive = false;
1485
1486 for raw_line in status.lines() {
1487 let line = match raw_line.strip_prefix("[GNUPG:] ") {
1488 Some(l) => l,
1489 None => continue,
1490 };
1491
1492 for entry in TABLE {
1493 let rest = match line.strip_prefix(entry.check) {
1494 Some(r) => r,
1495 None => continue,
1496 };
1497
1498 if entry.exclusive {
1499 if seen_exclusive {
1500 error_reset(sigc);
1502 return;
1503 }
1504 seen_exclusive = true;
1505 }
1506
1507 if let Some(r) = entry.result {
1508 sigc.result = r;
1509 }
1510
1511 let mut cursor = rest;
1512
1513 if entry.keyid {
1514 let (key, after) = split_at_space(cursor);
1515 sigc.key = Some(key.to_owned());
1516 if entry.uid && !after.is_empty() {
1517 let signer = after.split('\n').next().unwrap_or("");
1519 sigc.signer = Some(signer.to_owned());
1520 }
1521 }
1522
1523 if entry.trust {
1524 let level: String = cursor
1525 .chars()
1526 .take_while(|&c| c != ' ' && c != '\n')
1527 .collect();
1528 match TrustLevel::from_status(&level) {
1529 Some(t) => sigc.trust_level = t,
1530 None => {
1531 error_reset(sigc);
1532 return;
1533 }
1534 }
1535 }
1536
1537 if entry.fingerprint {
1538 let (fpr, mut after) = split_at_space(cursor);
1540 sigc.fingerprint = Some(fpr.to_owned());
1541 cursor = after;
1543 let mut remaining = 9;
1544 while remaining > 0 && !cursor.is_empty() {
1545 let (_, next) = split_at_space(cursor);
1546 after = next;
1547 if after.is_empty() {
1548 break;
1549 }
1550 cursor = after;
1551 remaining -= 1;
1552 }
1553 if remaining == 0 {
1554 let primary = cursor.split('\n').next().unwrap_or("");
1555 sigc.primary_key_fingerprint = Some(primary.to_owned());
1556 }
1557 }
1558
1559 break;
1560 }
1561 }
1562}
1563
1564fn error_reset(sigc: &mut SignatureCheck) {
1566 sigc.result = 'E';
1567 sigc.primary_key_fingerprint = None;
1568 sigc.fingerprint = None;
1569 sigc.signer = None;
1570 sigc.key = None;
1571}
1572
1573fn split_at_space(s: &str) -> (&str, &str) {
1575 match s.find(' ') {
1576 Some(i) => (&s[..i], &s[i + 1..]),
1577 None => (s, ""),
1578 }
1579}
1580
1581fn write_temp_file(data: &[u8]) -> Result<PathBuf> {
1583 write_temp_file_named(data, "git_vtag")
1584}
1585
1586fn temp_file_path(stem: &str) -> PathBuf {
1589 let dir = std::env::temp_dir();
1590 let unique = format!("{stem}_{}_{}", std::process::id(), next_temp_counter());
1591 dir.join(unique)
1592}
1593
1594fn write_temp_file_named(data: &[u8], stem: &str) -> Result<PathBuf> {
1596 let path = temp_file_path(stem);
1597 let mut f = std::fs::File::create(&path)
1598 .map_err(|e| Error::Signing(format!("could not create temporary file: {e}")))?;
1599 f.write_all(data)
1600 .map_err(|e| Error::Signing(format!("failed writing to temporary file: {e}")))?;
1601 Ok(path)
1602}
1603
1604fn next_temp_counter() -> u64 {
1606 use std::sync::atomic::{AtomicU64, Ordering};
1607 static COUNTER: AtomicU64 = AtomicU64::new(0);
1608 let now = crate::git_date::tm::process_wall_clock_sec() as u64;
1609 now ^ COUNTER.fetch_add(1, Ordering::Relaxed)
1610}
1611
1612pub fn committer_signing_default(committer_ident: &str) -> String {
1618 if let Some(angle_end) = committer_ident.find('>') {
1619 committer_ident[..=angle_end].to_owned()
1620 } else {
1621 committer_ident.to_owned()
1622 }
1623}
1624
1625pub fn parse_signed_buffer(buf: &[u8]) -> Option<(Vec<u8>, Vec<u8>)> {
1636 let size = buf.len();
1637 let mut len = 0usize;
1638 let mut matched: Option<usize> = None;
1639 while len < size {
1640 if GpgFormat::from_signature(&buf[len..]).is_some() {
1641 matched = Some(len);
1642 }
1643 let eol = memchr(buf, len, b'\n');
1644 len = match eol {
1645 Some(p) => p + 1,
1646 None => size,
1647 };
1648 }
1649 let m = matched?;
1650 Some((buf[..m].to_vec(), buf[m..].to_vec()))
1651}
1652
1653pub fn verify_tag(cfg: &GpgConfig, raw_tag: &[u8]) -> Result<SignatureCheck> {
1661 let (payload, signature) = match parse_signed_buffer(raw_tag) {
1662 Some(parts) => parts,
1663 None => return Ok(SignatureCheck::default_none()),
1664 };
1665
1666 let detected_format = GpgFormat::from_signature(&signature).unwrap_or(cfg.format);
1667
1668 if detected_format == GpgFormat::Ssh {
1669 return verify_ssh_signed_buffer(cfg, payload, signature);
1670 }
1671
1672 let program = resolve_program(&cfg.program_for(detected_format))?;
1673
1674 let sig_path = write_temp_file(&signature)?;
1675
1676 let mut args = vec![OsString::from("--status-fd=1")];
1677 for a in detected_format.verify_args() {
1678 args.push(OsString::from(a));
1679 }
1680 args.push(OsString::from("--verify"));
1681 args.push(OsString::from(sig_path.to_string_lossy().as_ref()));
1682 args.push(OsString::from("-"));
1683 let spec = CommandSpec {
1684 program: program.clone(),
1685 args,
1686 shell: None,
1687 cwd: None,
1688 env: CommandEnvironment::inherit_process_only(),
1689 stdin: stdin_pipe(&payload),
1690 stdout: CommandStdio::Pipe,
1691 stderr: CommandStdio::Pipe,
1692 };
1693
1694 let output = match signing_spawn(cfg, spec, None) {
1695 Ok(o) => o,
1696 Err(e) => {
1697 let _ = std::fs::remove_file(&sig_path);
1698 return Err(e);
1699 }
1700 };
1701 let _ = std::fs::remove_file(&sig_path);
1702
1703 let gpg_status = String::from_utf8_lossy(&output.stdout).into_owned();
1704 let human = String::from_utf8_lossy(&output.stderr).into_owned();
1705
1706 let mut sigc = SignatureCheck {
1707 signature,
1708 payload,
1709 result: 'N',
1710 trust_level: TrustLevel::Undefined,
1711 gpg_status: gpg_status.clone(),
1712 output: human,
1713 ..Default::default()
1714 };
1715
1716 parse_gpg_output(&mut sigc, &gpg_status);
1717
1718 Ok(sigc)
1719}
1720
1721#[cfg(test)]
1722mod tests {
1723 use super::*;
1724
1725 #[test]
1726 fn format_name_is_case_sensitive() {
1727 assert_eq!(GpgFormat::from_name("openpgp"), Some(GpgFormat::OpenPgp));
1728 assert_eq!(GpgFormat::from_name("x509"), Some(GpgFormat::X509));
1729 assert_eq!(GpgFormat::from_name("ssh"), Some(GpgFormat::Ssh));
1730 assert_eq!(GpgFormat::from_name("OpEnPgP"), None);
1731 assert_eq!(GpgFormat::from_name("OPENPGP"), None);
1732 }
1733
1734 #[test]
1735 fn add_header_signature_splices_gpgsig() {
1736 let commit = b"tree 0123\nparent 4567\nauthor a\ncommitter c\n\nmessage\n";
1737 let sig = b"-----BEGIN PGP SIGNATURE-----\nABC\n-----END PGP SIGNATURE-----\n";
1738 let out = add_header_signature(commit, sig, GPG_SIG_HEADER_SHA1);
1739 let text = String::from_utf8(out).unwrap();
1740 assert!(text.contains("\ncommitter c\ngpgsig -----BEGIN PGP SIGNATURE-----\n ABC\n -----END PGP SIGNATURE-----\n\nmessage\n"));
1741 }
1742
1743 #[test]
1744 fn extract_round_trips_signature() {
1745 let commit = b"tree 0123\nparent 4567\nauthor a\ncommitter c\n\nmessage\n";
1746 let sig = b"-----BEGIN PGP SIGNATURE-----\nABC\n-----END PGP SIGNATURE-----\n";
1747 let signed = add_header_signature(commit, sig, GPG_SIG_HEADER_SHA1);
1748 let (payload, extracted) = extract_signed_payload(&signed).unwrap();
1749 assert_eq!(payload, commit);
1750 assert_eq!(extracted, sig);
1751 }
1752
1753 #[test]
1754 fn extract_none_when_unsigned() {
1755 let commit = b"tree 0123\ncommitter c\n\nmsg\n";
1756 assert!(extract_signed_payload(commit).is_none());
1757 }
1758
1759 #[test]
1760 fn extra_headers_without_commit_signatures_drops_gpgsig_block() {
1761 let extra = b"mergetag foo\ngpgsig -----BEGIN PGP SIGNATURE-----\n ABC\n -----END PGP SIGNATURE-----\n";
1762 let stripped = extra_headers_without_commit_signatures(extra);
1763 assert_eq!(stripped, b"mergetag foo\n");
1764 }
1765
1766 #[test]
1767 fn parse_signed_buffer_splits_appended_tag_signature() {
1768 let body = b"object 0123\ntype commit\ntag v1\ntagger t <t@e> 1 +0000\n\nmessage\n";
1771 let sig = b"-----BEGIN SSH SIGNATURE-----\nAAAA\n-----END SSH SIGNATURE-----\n";
1772 let mut tag = body.to_vec();
1773 tag.extend_from_slice(sig);
1774 let (payload, signature) = parse_signed_buffer(&tag).expect("should split");
1775 assert_eq!(payload, body);
1776 assert_eq!(signature, sig);
1777 }
1778
1779 #[test]
1780 fn parse_signed_buffer_none_when_unsigned() {
1781 let body = b"object 0123\ntype commit\ntag v1\ntagger t <t@e> 1 +0000\n\nmessage\n";
1782 assert!(parse_signed_buffer(body).is_none());
1783 }
1784
1785 #[test]
1786 fn parse_goodsig_and_trust() {
1787 let status = "\
1788[GNUPG:] NEWSIG\n\
1789[GNUPG:] GOODSIG 73D758744BE721698EC54E8713D758744BE7216 C O Mitter <committer@example.com>\n\
1790[GNUPG:] VALIDSIG FINGERPRINT 2010-04-01 1270074988 0 4 0 17 2 00 PRIMARYFPR\n\
1791[GNUPG:] TRUST_ULTIMATE 0 pgp\n";
1792 let mut sigc = SignatureCheck::default_none();
1793 parse_gpg_output(&mut sigc, status);
1794 assert_eq!(sigc.result, 'G');
1795 assert_eq!(sigc.trust_level, TrustLevel::Ultimate);
1796 assert_eq!(
1797 sigc.signer.as_deref(),
1798 Some("C O Mitter <committer@example.com>")
1799 );
1800 assert_eq!(
1801 sigc.key.as_deref(),
1802 Some("73D758744BE721698EC54E8713D758744BE7216")
1803 );
1804 assert!(sigc.verify_status(None));
1805 assert!(sigc.verify_status(Some(TrustLevel::Ultimate)));
1806 assert!(sigc.verify_status(Some(TrustLevel::Marginal)));
1807 }
1808
1809 #[test]
1810 fn parse_badsig() {
1811 let status = "[GNUPG:] BADSIG KEYID Some Signer <s@example.com>\n";
1812 let mut sigc = SignatureCheck::default_none();
1813 parse_gpg_output(&mut sigc, status);
1814 assert_eq!(sigc.result, 'B');
1815 assert!(!sigc.is_good());
1816 }
1817
1818 #[test]
1819 fn double_exclusive_status_is_error() {
1820 let status = "[GNUPG:] GOODSIG K1 A <a@x>\n[GNUPG:] BADSIG K2 B <b@x>\n";
1821 let mut sigc = SignatureCheck::default_none();
1822 parse_gpg_output(&mut sigc, status);
1823 assert_eq!(sigc.result, 'E');
1824 }
1825
1826 #[test]
1827 fn min_trust_level_from_config_is_case_insensitive() {
1828 assert_eq!(
1829 TrustLevel::from_config("marginal"),
1830 Some(TrustLevel::Marginal)
1831 );
1832 assert_eq!(TrustLevel::from_config("FULLY"), Some(TrustLevel::Fully));
1833 assert_eq!(TrustLevel::from_config("bogus"), None);
1834 }
1835
1836 #[test]
1837 fn format_detected_from_signature_armor() {
1838 assert_eq!(
1839 GpgFormat::from_signature(b"-----BEGIN SSH SIGNATURE-----\nABC\n"),
1840 Some(GpgFormat::Ssh)
1841 );
1842 assert_eq!(
1843 GpgFormat::from_signature(b"-----BEGIN PGP SIGNATURE-----\n"),
1844 Some(GpgFormat::OpenPgp)
1845 );
1846 assert_eq!(
1847 GpgFormat::from_signature(b"-----BEGIN PGP MESSAGE-----\n"),
1848 Some(GpgFormat::OpenPgp)
1849 );
1850 assert_eq!(
1851 GpgFormat::from_signature(b"-----BEGIN SIGNED MESSAGE-----\n"),
1852 Some(GpgFormat::X509)
1853 );
1854 assert_eq!(GpgFormat::from_signature(b"garbage"), None);
1855 }
1856
1857 #[test]
1858 fn should_sign_commit_honors_override_and_config() {
1859 let mut cfg = ConfigSet::default();
1860 assert!(!should_sign_commit(&cfg, None).unwrap());
1861 assert!(should_sign_commit(&cfg, Some(true)).unwrap());
1862 assert!(!should_sign_commit(&cfg, Some(false)).unwrap());
1863
1864 cfg.add_command_override("commit.gpgsign", "true").unwrap();
1865 assert!(should_sign_commit(&cfg, None).unwrap());
1866 assert!(!should_sign_commit(&cfg, Some(false)).unwrap());
1867 }
1868
1869 #[test]
1870 fn should_sign_tag_honors_override_and_config() {
1871 let mut cfg = ConfigSet::default();
1872 assert!(!should_sign_tag(&cfg, None).unwrap());
1873 assert!(should_sign_tag(&cfg, Some(true)).unwrap());
1874 assert!(!should_sign_tag(&cfg, Some(false)).unwrap());
1875
1876 cfg.add_command_override("tag.gpgSign", "true").unwrap();
1877 assert!(should_sign_tag(&cfg, None).unwrap());
1878 assert!(!should_sign_tag(&cfg, Some(false)).unwrap());
1879 }
1880
1881 #[test]
1882 fn append_tag_signature_concatenates_armor() {
1883 let body = b"object 0\ntype commit\ntag v1\ntagger t <t@e> 1 +0000\n\nmsg\n";
1884 let sig = b"-----BEGIN SSH SIGNATURE-----\nX\n-----END SSH SIGNATURE-----\n";
1885 let out = append_tag_signature(body, sig);
1886 assert_eq!(out.len(), body.len() + sig.len());
1887 assert_eq!(&out[..body.len()], body);
1888 assert_eq!(&out[body.len()..], sig);
1889 }
1890
1891 #[test]
1892 fn committer_signing_default_strips_timestamp() {
1893 assert_eq!(
1894 committer_signing_default("T <t@example.com> 1700000000 +0000"),
1895 "T <t@example.com>"
1896 );
1897 }
1898
1899 #[test]
1900 fn resolve_signing_key_uses_no_date_committer_default() {
1901 let cfg = GpgConfig::from_config(&ConfigSet::default()).expect("gpg config");
1902 let key = cfg.resolve_signing_key(
1903 None,
1904 &committer_signing_default("T <t@example.com> 1700000000 +0000"),
1905 );
1906 assert_eq!(key, "T <t@example.com>");
1907 }
1908
1909 #[test]
1910 fn literal_ssh_key_detection() {
1911 assert_eq!(
1912 is_literal_ssh_key("key::ssh-ed25519 AAAA"),
1913 Some("ssh-ed25519 AAAA")
1914 );
1915 assert_eq!(
1916 is_literal_ssh_key("ssh-ed25519 AAAA"),
1917 Some("ssh-ed25519 AAAA")
1918 );
1919 assert_eq!(is_literal_ssh_key("/home/u/.ssh/id_ed25519"), None);
1920 }
1921
1922 #[test]
1923 fn parse_ssh_output_trusted_principal() {
1924 let mut sigc = SignatureCheck::default_none();
1925 sigc.output =
1926 "Good \"git\" signature for principal with number 1 with ED25519 key SHA256:ABC\n"
1927 .to_owned();
1928 parse_ssh_output(&mut sigc);
1929 assert_eq!(sigc.result, 'G');
1930 assert_eq!(sigc.trust_level, TrustLevel::Fully);
1931 assert_eq!(sigc.signer.as_deref(), Some("principal with number 1"));
1932 assert_eq!(sigc.key.as_deref(), Some("SHA256:ABC"));
1933 assert_eq!(sigc.fingerprint.as_deref(), Some("SHA256:ABC"));
1934 assert!(sigc.primary_key_fingerprint.is_none());
1935 }
1936
1937 #[test]
1938 fn parse_ssh_output_untrusted_unknown_key() {
1939 let mut sigc = SignatureCheck::default_none();
1940 sigc.output = "Good \"git\" signature with ED25519 key SHA256:XYZ\nNo principal matched.\n"
1943 .to_owned();
1944 parse_ssh_output(&mut sigc);
1945 assert_eq!(sigc.result, 'G');
1946 assert_eq!(sigc.trust_level, TrustLevel::Undefined);
1947 assert!(sigc.signer.is_none());
1948 assert_eq!(sigc.key.as_deref(), Some("SHA256:XYZ"));
1949 assert_eq!(sigc.fingerprint.as_deref(), Some("SHA256:XYZ"));
1950 }
1951
1952 #[test]
1953 fn parse_ssh_output_bad_signature() {
1954 let mut sigc = SignatureCheck::default_none();
1955 sigc.output = "Signature verification failed: incorrect signature\n".to_owned();
1956 parse_ssh_output(&mut sigc);
1957 assert_eq!(sigc.result, 'B');
1958 assert_eq!(sigc.trust_level, TrustLevel::Never);
1959 assert!(sigc.key.is_none());
1960 }
1961
1962 #[test]
1963 fn stripspace_keeps_line_terminators() {
1964 assert_eq!(stripspace("Good ... SHA256:FPR\n"), "Good ... SHA256:FPR\n");
1966 assert_eq!(stripspace("a \n\n\nb\n\n"), "a\n\nb\n");
1967 assert_eq!(stripspace(""), "");
1968 let mut out = stripspace("Good ... SHA256:FPR\n");
1971 out.push_str("No principal matched.\n");
1972 assert_eq!(out.lines().next(), Some("Good ... SHA256:FPR"));
1973 }
1974
1975 #[test]
1976 fn payload_committer_timestamp_parsed() {
1977 let payload =
1978 b"tree 0123\nauthor A <a@x> 1112912173 -0700\ncommitter C <c@x> 1112912273 +0200\n\nmsg\n";
1979 assert_eq!(payload_committer_timestamp(payload), Some(1112912273));
1980 let tag = b"object 0123\ntype commit\ntag v1\ntagger T <t@x> 1112912000 -0500\n\nmsg\n";
1982 assert_eq!(payload_committer_timestamp(tag), Some(1112912000));
1983 assert_eq!(payload_committer_timestamp(b"tree 0123\n\nmsg\n"), None);
1985 }
1986}