1use crate::error::{Error, Result};
29use crate::trust::TrustStore;
30
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum Verification {
34 Verified { signers: Vec<String> },
36 Unverified { signers: Vec<String> },
39}
40
41impl Verification {
42 pub fn is_verified(&self) -> bool {
44 matches!(self, Verification::Verified { .. })
45 }
46
47 pub fn signers(&self) -> &[String] {
50 match self {
51 Verification::Verified { signers } | Verification::Unverified { signers } => signers,
52 }
53 }
54}
55
56pub fn check(
61 plugin_name: &str,
62 signers: &[String],
63 trust: &TrustStore,
64 allow_untrusted: bool,
65) -> Result<Verification> {
66 let any_trusted = signers.iter().any(|s| trust.contains(s).unwrap_or(false));
67 if any_trusted {
68 Ok(Verification::Verified {
69 signers: signers.to_vec(),
70 })
71 } else if allow_untrusted {
72 Ok(Verification::Unverified {
73 signers: signers.to_vec(),
74 })
75 } else {
76 Err(Error::UntrustedPlugin {
77 name: plugin_name.to_string(),
78 })
79 }
80}
81
82pub fn verify_signature(artifact: &[u8], signature: &[u8], pubkey: &[u8]) -> Result<()> {
109 match load_librnp() {
110 Ok(lib) => verify_via_rnp(&lib, artifact, signature, pubkey),
111 Err(_load_err) => verify_via_gpg(artifact, signature, pubkey),
118 }
119}
120
121const LIBRNP_CANDIDATES: &[&str] = &["librnp.dylib", "librnp.so", "rnp.dll", "librnp"];
129
130fn load_librnp() -> std::result::Result<libloading::Library, String> {
131 let mut last: Option<String> = None;
132 for name in LIBRNP_CANDIDATES {
133 match unsafe { libloading::Library::new(*name) } {
134 Ok(lib) => return Ok(lib),
135 Err(e) => last = Some(format!("{name}: {e}")),
136 }
137 }
138 Err(last.unwrap_or_else(|| "no candidate names".to_string()))
139}
140
141fn verify_via_rnp(
156 lib: &libloading::Library,
157 artifact: &[u8],
158 signature: &[u8],
159 pubkey: &[u8],
160) -> Result<()> {
161 type RnpFfiCreateFn = unsafe extern "C" fn(
163 *mut ffi::RnpFfi,
164 *const std::os::raw::c_char,
165 *const std::os::raw::c_char,
166 ) -> ffi::RnpResult;
167 type RnpFfiDestroyFn = unsafe extern "C" fn(ffi::RnpFfi) -> ffi::RnpResult;
168 type RnpInputFromMemoryFn =
169 unsafe extern "C" fn(*mut ffi::RnpInput, *const u8, usize, ffi::RnpBool) -> ffi::RnpResult;
170 type RnpInputDestroyFn = unsafe extern "C" fn(ffi::RnpInput) -> ffi::RnpResult;
171 type RnpLoadKeysFn = unsafe extern "C" fn(
172 ffi::RnpFfi,
173 *const std::os::raw::c_char,
174 ffi::RnpInput,
175 u32,
176 ) -> ffi::RnpResult;
177 type RnpOpVerifyDetachedCreateFn = unsafe extern "C" fn(
178 *mut ffi::RnpOpVerify,
179 ffi::RnpFfi,
180 ffi::RnpInput,
181 ffi::RnpInput,
182 ) -> ffi::RnpResult;
183 type RnpOpVerifyExecuteFn = unsafe extern "C" fn(ffi::RnpOpVerify) -> ffi::RnpResult;
184 type RnpOpVerifyDestroyFn = unsafe extern "C" fn(ffi::RnpOpVerify) -> ffi::RnpResult;
185 type RnpOpVerifyGetSignatureCountFn =
186 unsafe extern "C" fn(ffi::RnpOpVerify, *mut usize) -> ffi::RnpResult;
187 type RnpOpVerifyGetSignatureAtFn =
188 unsafe extern "C" fn(ffi::RnpOpVerify, usize, *mut ffi::RnpOpVerifySig) -> ffi::RnpResult;
189 type RnpOpVerifySignatureGetStatusFn =
190 unsafe extern "C" fn(ffi::RnpOpVerifySig) -> ffi::RnpResult;
191
192 macro_rules! sym {
194 ($name:literal, $ty:ty) => {{
195 let name_str = std::str::from_utf8($name).unwrap_or("<non-utf8 symbol>");
196 match unsafe { lib.get::<$ty>($name) } {
197 Ok(f) => *f,
198 Err(e) => {
199 return Err(Error::RnpLoad {
200 message: format!("symbol {name_str} not found: {e}"),
201 });
202 }
203 }
204 }};
205 }
206
207 let ffi_create: RnpFfiCreateFn = sym!(b"rnp_ffi_create\0", RnpFfiCreateFn);
208 let ffi_destroy: RnpFfiDestroyFn = sym!(b"rnp_ffi_destroy\0", RnpFfiDestroyFn);
209 let input_from_memory: RnpInputFromMemoryFn =
210 sym!(b"rnp_input_from_memory\0", RnpInputFromMemoryFn);
211 let input_destroy: RnpInputDestroyFn = sym!(b"rnp_input_destroy\0", RnpInputDestroyFn);
212 let load_keys: RnpLoadKeysFn = sym!(b"rnp_load_keys\0", RnpLoadKeysFn);
213 let op_verify_detached_create: RnpOpVerifyDetachedCreateFn = sym!(
214 b"rnp_op_verify_detached_create\0",
215 RnpOpVerifyDetachedCreateFn
216 );
217 let op_verify_execute: RnpOpVerifyExecuteFn =
218 sym!(b"rnp_op_verify_execute\0", RnpOpVerifyExecuteFn);
219 let op_verify_destroy: RnpOpVerifyDestroyFn =
220 sym!(b"rnp_op_verify_destroy\0", RnpOpVerifyDestroyFn);
221 let get_sig_count: RnpOpVerifyGetSignatureCountFn = sym!(
222 b"rnp_op_verify_get_signature_count\0",
223 RnpOpVerifyGetSignatureCountFn
224 );
225 let get_sig_at: RnpOpVerifyGetSignatureAtFn = sym!(
226 b"rnp_op_verify_get_signature_at\0",
227 RnpOpVerifyGetSignatureAtFn
228 );
229 let get_sig_status: RnpOpVerifySignatureGetStatusFn = sym!(
230 b"rnp_op_verify_signature_get_status\0",
231 RnpOpVerifySignatureGetStatusFn
232 );
233
234 unsafe {
239 let mut ffi_handle: ffi::RnpFfi = std::ptr::null_mut();
240 let gpg = b"GPG\0";
241 let rc = (ffi_create)(
242 &mut ffi_handle as *mut ffi::RnpFfi,
243 gpg.as_ptr() as *const std::os::raw::c_char,
244 gpg.as_ptr() as *const std::os::raw::c_char,
245 );
246 if rc != ffi::RNP_SUCCESS || ffi_handle.is_null() {
247 return Err(Error::RnpVerify {
248 message: format!("rnp_ffi_create failed (rc={rc:#x})"),
249 });
250 }
251 struct FfiGuard {
253 handle: ffi::RnpFfi,
254 destroy: RnpFfiDestroyFn,
255 }
256 impl Drop for FfiGuard {
257 fn drop(&mut self) {
258 if !self.handle.is_null() {
259 unsafe { (self.destroy)(self.handle) };
260 }
261 }
262 }
263 let ffi_guard = FfiGuard {
264 handle: ffi_handle,
265 destroy: ffi_destroy,
266 };
267
268 let mut key_input: ffi::RnpInput = std::ptr::null_mut();
270 let rc = (input_from_memory)(
271 &mut key_input as *mut ffi::RnpInput,
272 pubkey.as_ptr(),
273 pubkey.len(),
274 ffi::RNP_TRUE,
275 );
276 if rc != ffi::RNP_SUCCESS {
277 return Err(Error::PublicKeyFormat {
278 path: "<bytes>".to_string(),
279 });
280 }
281 let key_guard = InputGuard {
282 handle: key_input,
283 destroy: input_destroy,
284 };
285 let rc = (load_keys)(
286 ffi_guard.handle,
287 gpg.as_ptr() as *const std::os::raw::c_char,
288 key_input,
289 ffi::RNP_LOAD_SAVE_PUBLIC_KEYS,
290 );
291 drop(key_guard);
292 if rc != ffi::RNP_SUCCESS {
293 return Err(Error::PublicKeyFormat {
294 path: "<bytes>".to_string(),
295 });
296 }
297
298 let mut data_input: ffi::RnpInput = std::ptr::null_mut();
300 let rc = (input_from_memory)(
301 &mut data_input as *mut ffi::RnpInput,
302 artifact.as_ptr(),
303 artifact.len(),
304 ffi::RNP_TRUE,
305 );
306 if rc != ffi::RNP_SUCCESS {
307 return Err(Error::RnpVerify {
308 message: format!("rnp_input_from_memory (artifact) failed (rc={rc:#x})"),
309 });
310 }
311 let data_guard = InputGuard {
312 handle: data_input,
313 destroy: input_destroy,
314 };
315
316 let mut sig_input: ffi::RnpInput = std::ptr::null_mut();
317 let rc = (input_from_memory)(
318 &mut sig_input as *mut ffi::RnpInput,
319 signature.as_ptr(),
320 signature.len(),
321 ffi::RNP_TRUE,
322 );
323 if rc != ffi::RNP_SUCCESS {
324 return Err(Error::SignatureFormat {
325 path: "<bytes>".to_string(),
326 });
327 }
328 let sig_guard = InputGuard {
329 handle: sig_input,
330 destroy: input_destroy,
331 };
332
333 let mut op: ffi::RnpOpVerify = std::ptr::null_mut();
334 let rc = (op_verify_detached_create)(
335 &mut op as *mut ffi::RnpOpVerify,
336 ffi_guard.handle,
337 data_input,
338 sig_input,
339 );
340 if rc != ffi::RNP_SUCCESS || op.is_null() {
341 return Err(Error::RnpVerify {
342 message: format!("rnp_op_verify_detached_create failed (rc={rc:#x})"),
343 });
344 }
345 let op_guard = OpVerifyGuard {
346 handle: op,
347 destroy: op_verify_destroy,
348 };
349
350 let rc = (op_verify_execute)(op);
354 if rc != ffi::RNP_SUCCESS {
355 return Err(Error::SignatureInvalid {
359 message: format!("rnp_op_verify_execute failed (rc={rc:#x})"),
360 });
361 }
362
363 let mut count: usize = 0;
365 let rc = (get_sig_count)(op, &mut count as *mut usize);
366 if rc != ffi::RNP_SUCCESS {
367 return Err(Error::RnpVerify {
368 message: format!("rnp_op_verify_get_signature_count failed (rc={rc:#x})"),
369 });
370 }
371 if count == 0 {
372 return Err(Error::SignatureInvalid {
373 message: "no signatures present".to_string(),
374 });
375 }
376
377 let mut last_status: u32 = 0;
378 let mut saw_valid = false;
379 for idx in 0..count {
380 let mut sig: ffi::RnpOpVerifySig = std::ptr::null_mut();
381 let rc = (get_sig_at)(op, idx, &mut sig as *mut ffi::RnpOpVerifySig);
382 if rc != ffi::RNP_SUCCESS {
383 return Err(Error::RnpVerify {
384 message: format!("rnp_op_verify_get_signature_at({idx}) failed (rc={rc:#x})"),
385 });
386 }
387 let status = (get_sig_status)(sig);
388 last_status = status;
389 if status == ffi::RNP_SUCCESS {
390 saw_valid = true;
391 }
392 }
393
394 drop(op_guard);
395 drop(sig_guard);
396 drop(data_guard);
397 drop(ffi_guard);
398
399 if saw_valid {
400 Ok(())
401 } else {
402 Err(Error::SignatureInvalid {
403 message: format!("no valid signature (last status={last_status:#x})"),
404 })
405 }
406 }
407}
408
409struct InputGuard {
410 handle: ffi::RnpInput,
411 destroy: unsafe extern "C" fn(ffi::RnpInput) -> ffi::RnpResult,
412}
413impl Drop for InputGuard {
414 fn drop(&mut self) {
415 if !self.handle.is_null() {
416 unsafe { (self.destroy)(self.handle) };
417 }
418 }
419}
420
421struct OpVerifyGuard {
422 handle: ffi::RnpOpVerify,
423 destroy: unsafe extern "C" fn(ffi::RnpOpVerify) -> ffi::RnpResult,
424}
425impl Drop for OpVerifyGuard {
426 fn drop(&mut self) {
427 if !self.handle.is_null() {
428 unsafe { (self.destroy)(self.handle) };
429 }
430 }
431}
432
433fn verify_via_gpg(artifact: &[u8], signature: &[u8], pubkey: &[u8]) -> Result<()> {
443 use std::io::Write;
444 use std::process::Command;
445
446 let scratch = std::env::temp_dir().join(format!(
450 "confium-verify-{}-{}",
451 std::process::id(),
452 scratch_counter()
453 ));
454 std::fs::create_dir_all(&scratch).map_err(|e| Error::VerificationSubprocess {
455 message: format!("failed to create tempdir {}: {e}", scratch.display()),
456 })?;
457
458 let key_path = scratch.join("pubkey.asc");
459 let data_path = scratch.join("artifact.bin");
460 let sig_path = scratch.join("sig.asc");
461 let gpg_home = scratch.join("gpghome");
462
463 let write_file = |path: &std::path::Path, body: &[u8], what: &str| -> Result<()> {
464 let mut f = std::fs::File::create(path).map_err(|e| Error::VerificationSubprocess {
465 message: format!("failed to create {what} {}: {e}", path.display()),
466 })?;
467 f.write_all(body)
468 .map_err(|e| Error::VerificationSubprocess {
469 message: format!("failed to write {what} {}: {e}", path.display()),
470 })?;
471 Ok(())
472 };
473
474 write_file(&key_path, pubkey, "pubkey")?;
475 write_file(&data_path, artifact, "artifact")?;
476 write_file(&sig_path, signature, "signature")?;
477
478 std::fs::create_dir_all(&gpg_home).map_err(|e| Error::VerificationSubprocess {
480 message: format!("failed to create gpghome: {e}"),
481 })?;
482 #[cfg(unix)]
483 {
484 use std::os::unix::fs::PermissionsExt;
485 std::fs::set_permissions(&gpg_home, std::fs::Permissions::from_mode(0o700)).map_err(
486 |e| Error::VerificationSubprocess {
487 message: format!("failed to chmod gpghome: {e}"),
488 },
489 )?;
490 }
491
492 let gnupg_arg = format!("{}", gpg_home.display());
493
494 let import = Command::new("gpg")
495 .args(["--homedir", &gnupg_arg, "--import"])
496 .arg(&key_path)
497 .output()
498 .map_err(|e| Error::VerificationSubprocess {
499 message: format!("failed to invoke gpg --import: {e}"),
500 })?;
501 if !import.status.success() {
502 let _ = std::fs::remove_dir_all(&scratch);
503 return Err(Error::VerificationSubprocess {
504 message: format!(
505 "gpg --import failed: {}",
506 String::from_utf8_lossy(&import.stderr).trim()
507 ),
508 });
509 }
510
511 let verify = Command::new("gpg")
512 .args(["--homedir", &gnupg_arg, "--verify"])
513 .arg(&sig_path)
514 .arg(&data_path)
515 .output()
516 .map_err(|e| Error::VerificationSubprocess {
517 message: format!("failed to invoke gpg --verify: {e}"),
518 })?;
519
520 let _ = std::fs::remove_dir_all(&scratch);
522
523 if verify.status.success() {
524 Ok(())
525 } else {
526 Err(Error::SignatureInvalid {
527 message: format!(
528 "gpg --verify rejected signature: {}",
529 String::from_utf8_lossy(&verify.stderr).trim()
530 ),
531 })
532 }
533}
534
535fn scratch_counter() -> u64 {
539 use std::sync::atomic::{AtomicU64, Ordering};
540 static COUNTER: AtomicU64 = AtomicU64::new(0);
541 COUNTER.fetch_add(1, Ordering::Relaxed)
542}
543
544mod ffi {
547 pub type RnpResult = u32;
548 pub type RnpBool = bool;
549
550 pub type RnpFfi = *mut std::os::raw::c_void;
551 pub type RnpInput = *mut std::os::raw::c_void;
552 pub type RnpOpVerify = *mut std::os::raw::c_void;
553 pub type RnpOpVerifySig = *mut std::os::raw::c_void;
554
555 pub const RNP_SUCCESS: RnpResult = 0;
557 pub const RNP_TRUE: RnpBool = true;
558
559 pub const RNP_LOAD_SAVE_PUBLIC_KEYS: u32 = 1 << 0;
561}
562
563#[cfg(test)]
564mod tests {
565 use super::*;
566 use crate::manifest::TrustRoot;
567 use crate::trust::TrustStore;
568 use std::path::PathBuf;
569 use tempfile::tempdir;
570
571 fn root(name: &str) -> TrustRoot {
572 TrustRoot {
573 name: name.to_string(),
574 key_id: "0x1".to_string(),
575 fingerprint: "AAAA".to_string(),
576 key_url: format!("/publishers/{}.asc", name),
577 }
578 }
579
580 fn store_at(dir: &tempfile::TempDir) -> TrustStore {
581 TrustStore::for_home(PathBuf::from(dir.path()))
582 }
583
584 #[test]
585 fn verifies_when_trusted_signer_present() {
586 let dir = tempdir().unwrap();
587 let store = store_at(&dir);
588 store.add(root("ribose")).unwrap();
589 let v = check("botan", &["ribose".to_string()], &store, false).unwrap();
590 assert!(v.is_verified());
591 }
592
593 #[test]
594 fn refuses_untrusted_without_override() {
595 let dir = tempdir().unwrap();
596 let store = store_at(&dir);
597 let err = check("botan", &["stranger".to_string()], &store, false).unwrap_err();
598 assert!(matches!(err, Error::UntrustedPlugin { .. }));
599 }
600
601 #[test]
602 fn allows_untrusted_with_override() {
603 let dir = tempdir().unwrap();
604 let store = store_at(&dir);
605 let v = check("botan", &["stranger".to_string()], &store, true).unwrap();
606 assert!(!v.is_verified());
607 assert_eq!(v.signers(), &["stranger"]);
608 }
609}
610
611#[cfg(test)]
612mod pgp_tests {
613 use super::*;
614 use std::fs;
615 use std::path::PathBuf;
616 use std::process::Command;
617 use tempfile::TempDir;
618
619 fn gpg_path() -> Option<PathBuf> {
624 super::which_shim::which("gpg").or_else(|| {
625 let out = Command::new("sh")
626 .args(["-c", "command -v gpg"])
627 .output()
628 .ok()?;
629 if !out.status.success() {
630 return None;
631 }
632 let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
633 if s.is_empty() {
634 None
635 } else {
636 Some(PathBuf::from(s))
637 }
638 })
639 }
640
641 struct Fixture {
645 gpg: PathBuf,
646 home: PathBuf,
647 _tmp: TempDir,
648 keyid: String,
649 }
650
651 impl Fixture {
652 fn new() -> Option<Self> {
653 let gpg = gpg_path()?;
654 let tmp = tempfile::tempdir().ok()?;
655 let home = tmp.path().join("gpghome");
656 fs::create_dir_all(&home).ok()?;
657 #[cfg(unix)]
658 {
659 use std::os::unix::fs::PermissionsExt;
660 fs::set_permissions(&home, fs::Permissions::from_mode(0o700)).ok()?;
661 }
662 let home_arg = format!("{}", home.display());
663
664 let batch = r#"Key-Type: RSA
666Key-Length: 1024
667Name-Real: Confium Test Publisher
668Name-Email: test@confium.example
669Expire-Date: 0
670%no-protection
671%commit
672"#;
673 let batch_path = tmp.path().join("batch");
674 fs::write(&batch_path, batch).ok()?;
675
676 let gen_out = Command::new(&gpg)
677 .args(["--homedir", &home_arg, "--batch", "--gen-key"])
678 .arg(&batch_path)
679 .output()
680 .ok()?;
681 if !gen_out.status.success() {
682 return None;
683 }
684
685 let list = Command::new(&gpg)
687 .args(["--homedir", &home_arg, "--list-keys", "--with-colons"])
688 .output()
689 .ok()?;
690 if !list.status.success() {
691 return None;
692 }
693 let stdout = String::from_utf8_lossy(&list.stdout);
694 let keyid = stdout.lines().find_map(|line| {
695 let mut fields = line.split(':');
696 if fields.next() == Some("pub") {
697 fields.nth(3).map(|s| s.to_string())
699 } else {
700 None
701 }
702 })?;
703
704 Some(Fixture {
705 gpg,
706 home,
707 _tmp: tmp,
708 keyid,
709 })
710 }
711
712 fn home_arg(&self) -> String {
713 format!("{}", self.home.display())
714 }
715
716 fn export_pubkey(&self) -> Vec<u8> {
718 let out = Command::new(&self.gpg)
719 .args([
720 "--homedir",
721 &self.home_arg(),
722 "--armor",
723 "--export",
724 &self.keyid,
725 ])
726 .output()
727 .expect("gpg --export");
728 assert!(out.status.success(), "gpg export failed");
729 out.stdout
730 }
731
732 fn sign_detached(&self, data: &[u8]) -> Vec<u8> {
735 let tmp = tempfile::tempdir().expect("tempdir");
736 let data_path = tmp.path().join("data.bin");
737 fs::write(&data_path, data).expect("write data");
738 let out = Command::new(&self.gpg)
739 .args([
740 "--homedir",
741 &self.home_arg(),
742 "--batch",
743 "--yes",
744 "--detach-sign",
745 "--armor",
746 ])
747 .arg(&data_path)
748 .output()
749 .expect("gpg --detach-sign");
750 assert!(out.status.success(), "gpg sign failed");
751 let sig_path = tmp.path().join("data.bin.asc");
752 fs::read(&sig_path).expect("read sig")
753 }
754 }
755
756 #[test]
757 fn verify_signature_accepts_valid_signature() {
758 let f = match Fixture::new() {
759 Some(f) => f,
760 None => {
761 eprintln!("skipping: gpg not available");
762 return;
763 }
764 };
765 let pubkey = f.export_pubkey();
766 let artifact = b"the quick brown fox jumps over the lazy dog";
767 let sig = f.sign_detached(artifact);
768 assert!(verify_signature(artifact, &sig, &pubkey).is_ok());
769 }
770
771 #[test]
772 fn verify_signature_rejects_tampered_artifact() {
773 let f = match Fixture::new() {
774 Some(f) => f,
775 None => {
776 eprintln!("skipping: gpg not available");
777 return;
778 }
779 };
780 let pubkey = f.export_pubkey();
781 let sig = f.sign_detached(b"original artifact bytes");
782 let tampered = b"modified artifact bytes";
783 let err = verify_signature(tampered, &sig, &pubkey).unwrap_err();
784 assert!(
785 matches!(
786 err,
787 Error::SignatureInvalid { .. } | Error::RnpVerify { .. }
788 ),
789 "unexpected error: {err:?}"
790 );
791 }
792
793 #[test]
794 fn verify_signature_rejects_wrong_pubkey() {
795 let f = match Fixture::new() {
796 Some(f) => f,
797 None => {
798 eprintln!("skipping: gpg not available");
799 return;
800 }
801 };
802 let artifact = b"some artifact";
804 let sig = f.sign_detached(artifact);
805 let other = Fixture::new().expect("second fixture");
807 let wrong_pubkey = other.export_pubkey();
808 let err = verify_signature(artifact, &sig, &wrong_pubkey).unwrap_err();
809 assert!(
810 matches!(
811 err,
812 Error::SignatureInvalid { .. } | Error::RnpVerify { .. }
813 ),
814 "unexpected error: {err:?}"
815 );
816 }
817
818 #[test]
819 fn verify_signature_rejects_garbage_signature() {
820 let f = match Fixture::new() {
821 Some(f) => f,
822 None => {
823 eprintln!("skipping: gpg not available");
824 return;
825 }
826 };
827 let pubkey = f.export_pubkey();
828 let garbage = b"not a real signature";
829 let err = verify_signature(b"artifact", garbage, &pubkey).unwrap_err();
830 assert!(
832 matches!(
833 err,
834 Error::SignatureInvalid { .. }
835 | Error::RnpVerify { .. }
836 | Error::SignatureFormat { .. }
837 ),
838 "unexpected error: {err:?}"
839 );
840 }
841
842 #[test]
844 fn gpg_fallback_accepts_valid_signature() {
845 let f = match Fixture::new() {
846 Some(f) => f,
847 None => {
848 eprintln!("skipping: gpg not available");
849 return;
850 }
851 };
852 let pubkey = f.export_pubkey();
853 let artifact = b"artifact bytes";
854 let sig = f.sign_detached(artifact);
855 assert!(verify_via_gpg(artifact, &sig, &pubkey).is_ok());
856 }
857}
858
859#[cfg(test)]
862mod which_shim {
863 pub fn which(name: &str) -> Option<std::path::PathBuf> {
865 let path = std::env::var_os("PATH")?;
866 for dir in std::env::split_paths(&path) {
867 let candidate = dir.join(name);
868 if candidate.is_file() {
869 return Some(candidate);
870 }
871 }
872 None
873 }
874}