1use crate::crypto::{
16 self, derive_checksum, generate_keypair, kdf, COMMENTHDR, KDFALG, KEYNUMLEN, PKALG, SALT_LEN,
17 SECKEY_LEN,
18};
19use crate::error::{Error, Result};
20use crate::file::open;
21use crate::file::parse_stream;
22use crate::file::write_stream;
23use crate::file::{parse, EncKey, PubKey, Sig};
24use crate::utils::check_keyname_compliance;
25use crate::utils::check_password_strength;
26use crate::utils::log_untrusted_buf;
27use crate::utils::read_password;
28use base64ct::{Base64, Encoding as _};
29use data_encoding::HEXLOWER;
30use memchr::memchr;
31use memchr::memmem;
32use rand_core::{OsRng, TryRngCore as _};
33use sha2::{Digest as _, Sha256, Sha512};
34use std::fs::File;
35use std::io::stdout;
36
37use std::io::Cursor;
38use std::io::{copy, stderr, stdin};
39use std::io::{Read, Seek, SeekFrom, Write};
40use std::path::{Path, PathBuf};
41use std::str;
42use zeroize::Zeroizing;
43
44type EmbeddedSigResult = Result<(Sig, Vec<u8>, Box<dyn Read>)>;
45
46struct GzipHeader {
48 flg: u8,
50 head: [u8; 10],
52 extra_field: Vec<u8>,
54 name_field: Vec<u8>,
56 comment_vec: Vec<u8>,
58}
59
60#[derive(Default)]
85pub struct KeyGenerator {
86 rounds: u32,
87 comment: Option<String>,
88 key_id: Option<i32>,
89 tty_handle: Option<File>,
90}
91
92impl KeyGenerator {
93 #[must_use]
95 pub fn new() -> Self {
96 Self {
97 rounds: crypto::DEFAULT_ROUNDS,
98 comment: None,
99 key_id: None,
100 tty_handle: None,
101 }
102 }
103
104 #[must_use]
106 pub fn tty_handle(mut self, tty: File) -> Self {
107 self.tty_handle = Some(tty);
108 self
109 }
110
111 #[must_use]
115 pub fn rounds(mut self, rounds: u32) -> Self {
116 self.rounds = rounds;
117 self
118 }
119
120 #[must_use]
124 pub fn comment(mut self, comment: impl Into<String>) -> Self {
125 self.comment = Some(comment.into());
126 self
127 }
128
129 #[must_use]
131 pub fn key_id(mut self, key_id: i32) -> Self {
132 self.key_id = Some(key_id);
133 self
134 }
135
136 pub fn generate_io<W1: Write, W2: Write>(
141 self,
142 mut pub_writer: W1,
143 mut sec_writer: W2,
144 ) -> Result<()> {
145 let comment = self.comment.as_deref().unwrap_or("signify");
146
147 let mut keynum = [0_u8; KEYNUMLEN];
148 OsRng.try_fill_bytes(&mut keynum)?;
149
150 let (public_key, secret_key) = generate_keypair()?;
151
152 let (enc_key, pub_key_struct) = if self.rounds > 0 {
153 let pass = prompt_password(true, self.key_id, self.tty_handle.as_ref())?;
154 let mut salt = [0u8; SALT_LEN];
155 OsRng.try_fill_bytes(&mut salt)?;
156
157 let mut xorkey = Zeroizing::new(vec![0u8; SECKEY_LEN]);
158 kdf(&pass, &salt, self.rounds, &mut xorkey)?;
159
160 let mut seckey = Zeroizing::new([0u8; SECKEY_LEN]);
161 for i in 0..SECKEY_LEN {
162 seckey[i] = secret_key[i] ^ xorkey[i];
163 }
164
165 let checksum = derive_checksum(secret_key.as_ref());
166 let enc_key = EncKey {
167 pkalg: PKALG,
168 kdfalg: KDFALG,
169 kdfrounds: self.rounds,
170 salt,
171 checksum,
172 keynum,
173 seckey,
174 };
175
176 let pk = PubKey {
177 pkalg: PKALG,
178 keynum,
179 pubkey: public_key,
180 };
181
182 (enc_key, pk)
183 } else {
184 let enc_key = EncKey {
185 pkalg: PKALG,
186 kdfalg: KDFALG,
187 kdfrounds: 0,
188 salt: [0u8; SALT_LEN],
189 checksum: derive_checksum(secret_key.as_ref()),
190 keynum,
191 seckey: Zeroizing::new(*secret_key),
192 };
193
194 let pk = PubKey {
195 pkalg: PKALG,
196 keynum,
197 pubkey: public_key,
198 };
199 (enc_key, pk)
200 };
201
202 let comment_len = comment
204 .len()
205 .checked_add(" secret key".len())
206 .ok_or(Error::Overflow)?;
207 let mut seckey_comment = Vec::with_capacity(comment_len);
208 seckey_comment.extend_from_slice(comment.as_bytes());
209 seckey_comment.extend_from_slice(b" secret key");
210 write_stream(&mut sec_writer, &seckey_comment, &enc_key.to_bytes())?;
211
212 let mut pubkey_comment = Vec::with_capacity(comment_len);
213 pubkey_comment.extend_from_slice(comment.as_bytes());
214 pubkey_comment.extend_from_slice(b" public key");
215 write_stream(&mut pub_writer, &pubkey_comment, &pub_key_struct.to_bytes())
216 }
217
218 pub fn generate(self, pubkey_path: &Path, seckey_path: &Path) -> Result<()> {
223 let pub_file = open(pubkey_path, true)?;
224 let sec_file = open(seckey_path, true)?;
225 self.generate_io(pub_file, sec_file)
226 }
227}
228
229pub struct Signer {
315 seckey: Option<PathBuf>,
316 embed: bool,
317 gzip: bool,
318 key_id: Option<i32>,
319 tty_handle: Option<File>,
320}
321
322impl Default for Signer {
323 fn default() -> Self {
324 Self::new()
325 }
326}
327
328impl Signer {
329 #[must_use]
331 pub fn new() -> Self {
332 Self {
333 seckey: None,
334 embed: false,
335 gzip: false,
336 key_id: None,
337 tty_handle: None,
338 }
339 }
340
341 #[must_use]
343 pub fn tty_handle(mut self, tty: File) -> Self {
344 self.tty_handle = Some(tty);
345 self
346 }
347
348 #[must_use]
350 pub fn seckey(mut self, path: impl Into<PathBuf>) -> Self {
351 self.seckey = Some(path.into());
352 self
353 }
354
355 #[must_use]
359 pub fn embed(mut self, embed: bool) -> Self {
360 self.embed = embed;
361 self
362 }
363
364 #[must_use]
368 pub fn gzip(mut self, gzip: bool) -> Self {
369 self.gzip = gzip;
370 self
371 }
372
373 #[must_use]
375 pub fn key_id(mut self, key_id: i32) -> Self {
376 self.key_id = Some(key_id);
377 self
378 }
379
380 pub fn sign(self, msg_path: &Path, sig_path: &Path) -> Result<()> {
385 self.sign_io(msg_path, sig_path, None, None, None)
386 }
387
388 pub fn sign_io(
390 &self,
391 msg_path: &Path,
392 sig_path: &Path,
393 msg_file: Option<File>,
394 sig_file: Option<File>,
395 seckey_file: Option<File>,
396 ) -> Result<()> {
397 let seckey_path = self.seckey.as_deref().ok_or(Error::RequiredArg("-s"))?;
398
399 let (enc_key, comment_bytes) = if let Some(f) = seckey_file {
400 parse_stream(f, EncKey::from_bytes)?
401 } else {
402 parse::<EncKey, _>(seckey_path, EncKey::from_bytes)?
403 };
404
405 let xorkey = if enc_key.kdfrounds > 0 {
406 let pass = Zeroizing::new(prompt_password(
407 false,
408 self.key_id,
409 self.tty_handle.as_ref(),
410 )?);
411 let mut xorkey = Zeroizing::new(vec![0u8; SECKEY_LEN]);
412 kdf(&pass, &enc_key.salt, enc_key.kdfrounds, &mut xorkey)?;
413 Some(xorkey)
414 } else {
415 None
416 };
417
418 let mut seckey = Zeroizing::new([0u8; SECKEY_LEN]); if let Some(x) = xorkey {
420 for i in 0..SECKEY_LEN {
421 seckey[i] = enc_key.seckey[i] ^ x[i];
422 }
423 } else {
424 seckey.copy_from_slice(enc_key.seckey.as_ref());
425 }
426
427 let checksum = derive_checksum(seckey.as_ref());
429 if checksum != enc_key.checksum {
430 return Err(Error::IncorrectPassphrase);
431 }
432
433 let is_stdout = sig_path.to_str() == Some("-");
435 let is_stdin = msg_path.to_str() == Some("-");
436
437 if self.gzip {
438 if self.embed {
439 return Err(Error::Io(std::io::Error::new(
440 std::io::ErrorKind::InvalidInput,
441 "cannot combine -e (embed) and -z (gzip)",
442 )));
443 }
444 return sign_gzip(
445 seckey.as_ref(),
446 enc_key.keynum,
447 seckey_path,
448 msg_path,
449 sig_path,
450 &comment_bytes,
451 msg_file,
452 sig_file,
453 );
454 }
455
456 let mut sig_comment = Vec::new();
458 if seckey_path.to_str() == Some("-") {
459 sig_comment.extend_from_slice(b"signature from ");
460 sig_comment.extend_from_slice(&comment_bytes);
461 } else {
462 sig_comment.extend_from_slice(b"verify with ");
463 sig_comment.extend_from_slice(
464 seckey_path
465 .file_stem()
466 .unwrap_or(seckey_path.as_os_str())
467 .as_encoded_bytes(),
468 );
469 sig_comment.extend_from_slice(b".pub");
470 };
471
472 sign_standard(SignParams {
473 seckey: &seckey,
474 keynum: enc_key.keynum,
475 msg_path,
476 sig_path,
477 embed: self.embed,
478 is_stdout,
479 is_stdin,
480 sig_comment: &sig_comment,
481 msg_file,
482 sig_file,
483 })
484 }
485}
486
487pub struct Verifier {
617 pubkey: Option<PathBuf>,
618 quiet: bool,
619 embed: bool,
620 gzip: bool,
621}
622
623impl Default for Verifier {
624 fn default() -> Self {
625 Self::new()
626 }
627}
628
629impl Verifier {
630 #[must_use]
632 pub fn new() -> Self {
633 Self {
634 pubkey: None,
635 quiet: false,
636 embed: false,
637 gzip: false,
638 }
639 }
640
641 #[must_use]
643 pub fn pubkey(mut self, path: impl Into<PathBuf>) -> Self {
644 self.pubkey = Some(path.into());
645 self
646 }
647
648 #[must_use]
652 pub fn quiet(mut self, quiet: bool) -> Self {
653 self.quiet = quiet;
654 self
655 }
656
657 #[must_use]
661 pub fn embed(mut self, embed: bool) -> Self {
662 self.embed = embed;
663 self
664 }
665
666 #[must_use]
670 pub fn gzip(mut self, gzip: bool) -> Self {
671 self.gzip = gzip;
672 self
673 }
674
675 pub fn verify(self, msg_path: &Path, sig_path: &Path) -> Result<()> {
680 self.verify_io(msg_path, sig_path, None, None, None)
681 }
682
683 pub fn verify_io(
685 self,
686 msg_path: &Path,
687 sig_path: &Path,
688 mut msg_file: Option<File>,
689 mut sig_file: Option<File>,
690 pubkey_file: Option<File>,
691 ) -> Result<()> {
692 if self.gzip {
693 return verify_gzip(
694 self.pubkey.as_deref(),
695 msg_path,
696 sig_path,
697 self.quiet,
698 msg_file,
699 sig_file,
700 pubkey_file,
701 );
702 }
703
704 let (sig, stream, output_path) = if self.embed {
707 let (sig, prelude, rest_reader) = if let Some(f) = sig_file.take() {
710 parse_embedded_signature_reader(Box::new(f))?
711 } else {
712 parse_embedded_signature(sig_path)?
713 };
714 let stream = Cursor::new(prelude).chain(rest_reader);
715
716 let out_path = if self.embed { Some(msg_path) } else { None };
717 (sig, Box::new(stream) as Box<dyn Read>, out_path)
718 } else {
719 let (sig, _comment) = if let Some(f) = sig_file.take() {
721 let (sig, comment) = parse_stream(f, Sig::from_bytes)?;
722 (sig, comment)
723 } else {
724 parse::<Sig, _>(sig_path, Sig::from_bytes)?
725 };
726
727 let file: Box<dyn Read> = if let Some(f) = msg_file.take() {
728 Box::new(f)
729 } else {
730 Box::new(open(msg_path, false)?)
731 };
732
733 (sig, file, None)
735 };
736
737 let pubkey = if let Some(f) = pubkey_file {
738 let (pk, _) = parse_stream(f, PubKey::from_bytes)?;
739 pk
740 } else if let Some(path) = &self.pubkey {
741 let (pk, _) = parse::<PubKey, _>(path, PubKey::from_bytes)?;
742 pk
743 } else {
744 return Err(Error::MissingPubKey);
745 };
746
747 if sig.keynum != pubkey.keynum {
748 return Err(Error::KeyMismatch);
749 }
750
751 let mut writer: Option<Box<dyn Write>> = if let Some(f) = msg_file.take() {
753 Some(Box::new(f))
755 } else if let Some(path) = output_path {
756 if path.to_str() == Some("-") {
758 Some(Box::new(stdout()))
759 } else {
760 Some(Box::new(open(path, true)?))
761 }
762 } else {
763 None
764 };
765
766 let writer_ref = writer.as_mut().map(|w| &mut **w as &mut dyn Write);
768 crypto::verify_stream(stream, writer_ref, &pubkey.pubkey, &sig.sig)?;
769
770 if !self.quiet {
771 println!("Signature Verified");
772 }
773
774 Ok(())
775 }
776}
777
778fn parse_embedded_signature_reader(mut reader: Box<dyn Read>) -> EmbeddedSigResult {
779 const HEADER_LIMIT: usize = 4096;
781 let mut buffer = vec![0_u8; HEADER_LIMIT];
782 let mut valid_len = 0;
783
784 while valid_len < HEADER_LIMIT {
786 let n = reader.read(&mut buffer[valid_len..]).map_err(Error::Io)?;
787 if n == 0 {
788 break;
789 }
790 valid_len = valid_len.checked_add(n).ok_or(Error::Overflow)?;
791 }
792 buffer.truncate(valid_len);
793
794 let n1 = memchr(b'\n', &buffer).ok_or(Error::InvalidCommentHeader)?;
795 let n2_start = n1.checked_add(1).ok_or(Error::Overflow)?;
796 let n2 = memchr(b'\n', &buffer[n2_start..]).ok_or(Error::MissingSignatureNewline)?;
797 let b64_start = n2_start;
798 let b64_end = b64_start.checked_add(n2).ok_or(Error::Overflow)?;
799
800 if b64_end > buffer.len() {
801 return Err(Error::InvalidCommentHeader);
802 }
803
804 let b64_bytes = &buffer[b64_start..b64_end];
805 let b64_str = str::from_utf8(b64_bytes).map_err(|_e| Error::InvalidSignatureUtf8)?;
806 let sig_bytes = Base64::decode_vec(b64_str.trim()).map_err(Error::Base64Decode)?;
807 let sig = Sig::from_bytes(&sig_bytes)?;
808
809 let msg_start = b64_end.checked_add(1).ok_or(Error::Overflow)?;
810
811 let prelude = if msg_start < buffer.len() {
813 buffer[msg_start..].to_vec()
814 } else {
815 Vec::new()
816 };
817
818 Ok((sig, prelude, reader))
819}
820
821fn prompt_password(
823 confirm: bool,
824 key_id: Option<i32>,
825 tty: Option<&File>,
826) -> Result<Zeroizing<Vec<u8>>> {
827 let pass = loop {
828 let pass = read_password("passphrase: ", key_id, tty)?;
829
830 if !confirm {
833 return Ok(pass);
834 }
835
836 match check_password_strength(&pass) {
837 Ok(()) if key_id.is_some() => return Ok(pass),
838 Err(error) if key_id.is_some() => return Err(error),
839 Err(error) => {
840 let mut msg = "signify: ".to_string();
841 msg.push_str(&error.to_string());
842 msg.push('\n');
843 stderr().write_all(msg.as_bytes()).map_err(Error::Io)?;
844 stderr().flush().map_err(Error::Io)?;
845 continue;
846 }
847 _ => break pass,
848 }
849 };
850
851 for _ in 0..3 {
852 stderr().flush().map_err(Error::Io)?;
853 let pass2 = read_password("confirm passphrase: ", None, tty)?;
854 if pass == pass2 {
855 return Ok(pass);
856 }
857 let mut msg = "signify: ".to_string();
858 msg.push_str(&Error::PasswordMismatch.to_string());
859 msg.push('\n');
860 stderr().write_all(msg.as_bytes()).map_err(Error::Io)?;
861 stderr().flush().map_err(Error::Io)?;
862 }
863
864 Err(Error::PasswordMismatch)
865}
866
867struct SignParams<'a> {
869 seckey: &'a [u8; 64],
871 keynum: [u8; 8],
873 msg_path: &'a Path,
875 sig_path: &'a Path,
877 embed: bool,
879 is_stdout: bool,
881 is_stdin: bool,
883 sig_comment: &'a [u8],
885 msg_file: Option<File>,
887 sig_file: Option<File>,
889}
890
891fn sign_standard(params: SignParams) -> Result<()> {
892 let make_header = |sig_bytes: &[u8]| -> Result<Vec<u8>> {
894 #[expect(clippy::disallowed_methods)]
896 let sig = Sig {
897 pkalg: PKALG,
898 keynum: params.keynum,
899 sig: sig_bytes.try_into().unwrap(),
900 };
901 let encoded = Base64::encode_string(&sig.to_bytes());
902 let mut h = Vec::new();
903 h.extend_from_slice(COMMENTHDR.as_bytes());
904 h.extend_from_slice(params.sig_comment);
905 h.push(b'\n');
906 h.extend_from_slice(encoded.as_bytes());
907 h.push(b'\n');
908 Ok(h)
909 };
910
911 let out_file = if let Some(f) = params.sig_file {
912 Some(f)
913 } else if params.is_stdout {
914 None
915 } else {
916 Some(open(params.sig_path, true)?)
917 };
918
919 if params.embed {
920 if let Some(mut file) = out_file {
921 let dummy_sig = [0u8; 64];
924 let header = make_header(&dummy_sig)?;
925 file.write_all(&header).map_err(Error::Io)?;
926
927 let mut reader: Box<dyn Read> = if let Some(f) = params.msg_file {
929 Box::new(f)
930 } else if params.is_stdin {
931 Box::new(stdin())
932 } else {
933 Box::new(open(params.msg_path, false)?)
934 };
935
936 let mut buf = Vec::new();
937 reader.read_to_end(&mut buf).map_err(Error::Io)?;
938 file.write_all(&buf).map_err(Error::Io)?;
939
940 let real_sig = crypto::sign(&buf, params.seckey)?;
942 let real_header = make_header(real_sig.as_ref())?;
943 if real_header.len() != header.len() {
944 return Err(Error::InvalidSignatureLength);
945 }
946 file.rewind().map_err(Error::Io)?;
947 file.write_all(&real_header).map_err(Error::Io)?;
948 } else {
949 if params.is_stdin {
951 let mut buf = Vec::new();
952 stdin().read_to_end(&mut buf).map_err(Error::Io)?;
953
954 let sig = crypto::sign(&buf, params.seckey)?;
955 let header = make_header(sig.as_ref())?;
956
957 let mut out = stdout();
958 out.write_all(&header).map_err(Error::Io)?;
959 out.write_all(&buf).map_err(Error::Io)?;
960 } else {
961 let mut file = if let Some(file) = params.msg_file {
963 file
964 } else {
965 open(params.msg_path, false)?
966 };
967
968 let _ = file.rewind();
971
972 let sig = crypto::sign_stream(&mut file, params.seckey)?;
973 let header = make_header(&sig)?;
974 let mut out = stdout();
975 out.write_all(&header).map_err(Error::Io)?;
976
977 file.rewind().map_err(Error::Io)?;
979 copy(&mut file, &mut out).map_err(Error::Io)?;
980 }
981 }
982 } else {
983 let mut reader: Box<dyn Read> = if let Some(f) = params.msg_file {
985 Box::new(f)
986 } else if params.is_stdin {
987 Box::new(stdin())
988 } else {
989 Box::new(open(params.msg_path, false)?)
990 };
991 let sig = crypto::sign_stream(&mut reader, params.seckey)?;
992 let header = make_header(&sig)?;
993
994 if let Some(mut f) = out_file {
995 f.write_all(&header).map_err(Error::Io)?;
996 } else {
997 stdout().write_all(&header).map_err(Error::Io)?;
998 }
999 }
1000
1001 Ok(())
1002}
1003
1004fn parse_embedded_signature(path: &Path) -> EmbeddedSigResult {
1009 let reader: Box<dyn Read> = if path.to_str() == Some("-") {
1010 Box::new(stdin())
1011 } else {
1012 Box::new(open(path, false)?)
1013 };
1014 parse_embedded_signature_reader(reader)
1015}
1016
1017pub fn check_checksums(
1026 pubkey_path: Option<&Path>,
1027 sig_path: &Path,
1028 mut sig_file: Option<File>,
1029 quiet: bool,
1030) -> Result<()> {
1031 let (sig, mut prelude, mut reader) = if let Some(f) = sig_file.take() {
1032 parse_embedded_signature_reader(Box::new(f))?
1033 } else {
1034 parse_embedded_signature(sig_path)?
1035 };
1036 reader.read_to_end(&mut prelude).map_err(Error::Io)?;
1037 let msg = prelude;
1038
1039 let path = pubkey_path.ok_or(Error::MissingPubKey)?;
1041 let (pubkey, _) = parse::<PubKey, _>(path, PubKey::from_bytes)?;
1042
1043 if sig.keynum != pubkey.keynum {
1044 return Err(Error::KeyMismatch);
1045 }
1046
1047 crypto::verify(&msg, &pubkey.pubkey, &sig.sig)?;
1048
1049 let mut failed = false;
1050 for (line_num, line) in msg.split(|&b| matches!(b, b'\n' | b'\r')).enumerate() {
1051 let line = line.trim_ascii();
1052 if line.is_empty() {
1053 continue;
1054 }
1055
1056 if let Err(error) = verify_checksum_line(line, quiet) {
1057 eprintln!("signify: line {}: {error}", line_num.saturating_add(1));
1058 failed = true;
1059 }
1060 }
1061
1062 if failed {
1063 Err(Error::CheckFailed)
1064 } else {
1065 Ok(())
1066 }
1067}
1068
1069pub fn verify_checksum_line(line: &[u8], quiet: bool) -> Result<()> {
1081 let (algo, hash, path) = parse_checksum_line(line)
1082 .ok_or_else(|| Error::ChecksumParseFailed(log_untrusted_buf(line)))?;
1083 verify_file_with_hash(algo, hash, &path, quiet)
1084}
1085
1086fn parse_checksum_line(line: &[u8]) -> Option<(&[u8], &[u8], &str)> {
1090 try_parse_bsd_format(line).or_else(|| try_parse_gnu_format(line))
1091}
1092
1093fn try_parse_bsd_format(line: &[u8]) -> Option<(&[u8], &[u8], &str)> {
1095 let marker = b" = ";
1096 let idx = memmem::find(line, marker)?;
1097
1098 let left = &line[..idx];
1099 let right_start = idx.checked_add(marker.len())?;
1100 let right = &line[right_start..];
1101 let hash = right.trim_ascii();
1102
1103 let space_idx = memchr(b' ', left)?;
1105 let algo = &left[..space_idx];
1106 let rest_start = space_idx.checked_add(1)?;
1107 let rest = &left[rest_start..];
1108
1109 if rest.len() < 2 || rest.first() != Some(&b'(') || rest.last() != Some(&b')') {
1111 return None;
1112 }
1113
1114 let filename = &rest[1..rest.len().checked_sub(1)?];
1115 let filename = std::str::from_utf8(filename).ok()?;
1116 if !filename.is_empty() {
1117 Some((algo, hash, filename))
1118 } else {
1119 None
1120 }
1121}
1122
1123fn try_parse_gnu_format(line: &[u8]) -> Option<(&[u8], &[u8], &str)> {
1127 let (sep_idx, sep_len) = if let Some(idx) = memmem::find(line, b" ") {
1129 (idx, 2)
1130 } else if let Some(idx) = memmem::find(line, b" *") {
1131 (idx, 2)
1132 } else {
1133 return None;
1134 };
1135
1136 let hash = &line[..sep_idx];
1137 let path = &line[sep_idx.checked_add(sep_len)?..];
1138 let path = std::str::from_utf8(path).ok()?;
1139
1140 let algo: &[u8] = match hash.len() {
1143 64 => b"SHA256",
1144 128 => b"SHA512",
1145 _ => return None,
1146 };
1147 if !hash.iter().all(|&b| b.is_ascii_hexdigit()) {
1148 return None;
1149 }
1150
1151 Some((algo, hash, path))
1152}
1153
1154fn verify_file_with_hash(algo: &[u8], hash: &[u8], path: &str, quiet: bool) -> Result<()> {
1158 let name = log_untrusted_buf(path.as_bytes());
1159 let path = Path::new(path);
1160
1161 let mut file = open(path, false).inspect_err(|error| {
1162 if !quiet {
1163 println!("{name}: FAIL: {error}");
1164 }
1165 })?;
1166
1167 const BUF_SIZE: usize = 64 * 1024;
1168 let mut buf = [0_u8; BUF_SIZE];
1169
1170 macro_rules! compute_hash {
1172 ($hasher:expr) => {{
1173 let mut hasher = $hasher;
1174 loop {
1175 match file.read(&mut buf) {
1176 Ok(0) => break,
1177 Ok(n) => hasher.update(&buf[..n]),
1178 Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1179 Err(error) => {
1180 let error = Error::Io(error);
1181 if !quiet {
1182 println!("{name}: FAIL: {error}");
1183 }
1184 return Err(error);
1185 }
1186 }
1187 }
1188 HEXLOWER.encode(&hasher.finalize())
1189 }};
1190 }
1191
1192 let calculated_hash = match algo {
1193 b"SHA256" => compute_hash!(Sha256::new()),
1194 b"SHA512" => compute_hash!(Sha512::new()),
1195 _ => {
1196 let algo = String::from_utf8_lossy(algo).to_string();
1197 let error = Error::UnsupportedHashAlgo(algo);
1198 if !quiet {
1199 println!("{name}: FAIL: {error}");
1200 }
1201 return Err(error);
1202 }
1203 };
1204
1205 if calculated_hash.as_bytes().eq_ignore_ascii_case(hash) {
1206 if !quiet {
1207 println!("{name}: OK");
1208 }
1209 Ok(())
1210 } else {
1211 let error = Error::ChecksumMismatch(name);
1212 if !quiet {
1213 println!("{error}");
1214 }
1215 Err(error)
1216 }
1217}
1218
1219fn read_gzip_header(sig_file: &mut dyn Read) -> Result<GzipHeader> {
1221 let mut head = [0u8; 10];
1222 sig_file.read_exact(&mut head).map_err(Error::Io)?;
1223
1224 if head[0] != 0x1f || head[1] != 0x8b {
1225 return Err(Error::Io(std::io::Error::new(
1226 std::io::ErrorKind::InvalidData,
1227 "Not a gzip file",
1228 )));
1229 }
1230 let flg = head[3];
1231 if (flg & 16) == 0 {
1232 return Err(Error::Io(std::io::Error::new(
1233 std::io::ErrorKind::InvalidData,
1234 "Unsigned gzip archive (no comment)",
1235 )));
1236 }
1237
1238 let mut extra_field = Vec::new();
1240 if (flg & 4) != 0 {
1241 let mut xlen_b = [0u8; 2];
1242 sig_file.read_exact(&mut xlen_b).map_err(Error::Io)?;
1243 extra_field.extend_from_slice(&xlen_b);
1244 let xlen = u64::from(u16::from_le_bytes(xlen_b));
1245 let mut reader = sig_file.take(xlen);
1246 reader.read_to_end(&mut extra_field).map_err(Error::Io)?;
1247 }
1248
1249 let mut name_field = Vec::new();
1250 if (flg & 8) != 0 {
1251 let mut buf = [0u8; 1];
1252 loop {
1253 sig_file.read_exact(&mut buf).map_err(Error::Io)?;
1254 name_field.push(buf[0]);
1255 if buf[0] == 0 {
1256 break;
1257 }
1258 }
1259 }
1260
1261 let mut comment_vec = Vec::new();
1263 let mut buf = [0u8; 1];
1264 loop {
1265 sig_file.read_exact(&mut buf).map_err(Error::Io)?;
1266 if buf[0] == 0 {
1267 break;
1268 }
1269 comment_vec.push(buf[0]);
1270 }
1271
1272 Ok(GzipHeader {
1273 flg,
1274 head,
1275 extra_field,
1276 name_field,
1277 comment_vec,
1278 })
1279}
1280
1281pub fn sign_gzip(
1283 seckey: &[u8],
1284 keynum: [u8; 8],
1285 seckey_path: &Path,
1286 msg_path: &Path,
1287 sig_path: &Path,
1288 comment_bytes: &[u8],
1289 msg_file: Option<std::fs::File>,
1290 sig_file: Option<std::fs::File>,
1291) -> Result<()> {
1292 let is_stdout = sig_path.as_os_str() == "-";
1293 let is_stdin = msg_path.as_os_str() == "-";
1294
1295 if is_stdin && msg_file.is_none() {
1296 return Err(Error::Io(std::io::Error::new(
1297 std::io::ErrorKind::InvalidInput,
1298 "Gzip signing requires a regular file input (not stdin)",
1299 )));
1300 }
1301
1302 let mut f = if let Some(f) = msg_file {
1303 f
1304 } else {
1305 open(msg_path, false)?
1306 };
1307
1308 let (head, data_start) = parse_gzip_for_signing(&mut f)?;
1309
1310 let (header_msg, _) = hash_gzip_content(&mut f, data_start, seckey_path)?;
1312
1313 let kp = ed25519_compact::KeyPair {
1315 pk: ed25519_compact::PublicKey::from_slice(&seckey[32..]).map_err(Error::Crypto)?,
1316 sk: ed25519_compact::SecretKey::from_slice(seckey).map_err(Error::Crypto)?,
1317 };
1318 let sig = kp.sk.sign(&header_msg, None);
1319
1320 let sig_comment = make_sig_comment(seckey_path, comment_bytes)?;
1321
1322 let sig_header = {
1323 let sig = Sig {
1324 pkalg: PKALG,
1325 keynum,
1326 sig: sig
1327 .as_ref()
1328 .try_into()
1329 .map_err(|_| Error::InvalidSignatureLength)?, };
1331 let encoded = Base64::encode_string(&sig.to_bytes());
1332 let mut h = Vec::new();
1333 h.extend_from_slice(COMMENTHDR.as_bytes());
1334 h.extend_from_slice(&sig_comment);
1335 h.push(b'\n');
1336 h.extend_from_slice(encoded.as_bytes());
1337 h.push(b'\n');
1338 h
1339 };
1340
1341 let mut out: Box<dyn Write> = if let Some(f) = sig_file {
1342 Box::new(f)
1343 } else if is_stdout {
1344 Box::new(stdout())
1345 } else {
1346 Box::new(open(sig_path, true)?)
1347 };
1348
1349 write_signed_gzip(
1350 &mut out,
1351 &head,
1352 &sig_header,
1353 &header_msg,
1354 &mut f,
1355 data_start,
1356 )
1357}
1358
1359fn parse_gzip_for_signing(f: &mut std::fs::File) -> Result<([u8; 10], u64)> {
1360 let mut head = [0u8; 10];
1361 f.read_exact(&mut head).map_err(Error::Io)?;
1362
1363 if head[0] != 0x1f || head[1] != 0x8b {
1364 return Err(Error::Io(std::io::Error::new(
1365 std::io::ErrorKind::InvalidData,
1366 "Not a gzip file",
1367 )));
1368 }
1369
1370 let flg = head[3];
1371 if (flg & 4) != 0 {
1372 let mut xlen_b = [0u8; 2];
1373 f.read_exact(&mut xlen_b).map_err(Error::Io)?;
1374 let xlen = i64::from(u16::from_le_bytes(xlen_b));
1375 f.seek(SeekFrom::Current(xlen)).map_err(Error::Io)?;
1376 }
1377
1378 if (flg & 8) != 0 {
1379 let mut buf = [0u8; 1];
1380 loop {
1381 f.read_exact(&mut buf).map_err(Error::Io)?;
1382 if buf[0] == 0 {
1383 break;
1384 }
1385 }
1386 }
1387
1388 if (flg & 16) != 0 {
1389 let mut buf = [0u8; 1];
1390 loop {
1391 f.read_exact(&mut buf).map_err(Error::Io)?;
1392 if buf[0] == 0 {
1393 break;
1394 }
1395 }
1396 }
1397
1398 if (flg & 2) != 0 {
1399 f.seek(SeekFrom::Current(2)).map_err(Error::Io)?;
1400 }
1401
1402 let data_start = f.stream_position().map_err(Error::Io)?;
1403 Ok((head, data_start))
1404}
1405
1406fn hash_gzip_content(
1407 f: &mut std::fs::File,
1408 data_start: u64,
1409 seckey_path: &Path,
1410) -> Result<(Vec<u8>, usize)> {
1411 const BLK_SIZE: usize = 0x0001_0000;
1412 let mut block = vec![0u8; BLK_SIZE];
1413
1414 loop {
1416 match f.read(&mut block) {
1417 Ok(0) => break,
1418 Ok(_) => continue,
1419 Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1420 Err(e) => return Err(Error::Io(e)),
1421 }
1422 }
1423
1424 let mut header_msg = Vec::new();
1425 let time_now = "0000-00-00T00:00:00Z";
1426 let keyname = if seckey_path.as_os_str() == "-" {
1427 "stdin"
1428 } else {
1429 seckey_path.to_str().ok_or(Error::InvalidPath)?
1430 };
1431 write!(
1432 &mut header_msg,
1433 "date={time_now}\nkey={keyname}\nalgorithm=SHA256\nblocksize={BLK_SIZE}\n\n",
1434 )
1435 .map_err(Error::Io)?;
1436
1437 f.seek(SeekFrom::Start(data_start)).map_err(Error::Io)?;
1438 loop {
1439 match f.read(&mut block) {
1440 Ok(0) => break,
1441 Ok(n) => {
1442 let hash = Sha256::digest(&block[..n]);
1443 let hex = HEXLOWER.encode(&hash);
1444 header_msg.extend_from_slice(hex.as_bytes());
1445 header_msg.push(b'\n');
1446 }
1447 Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1448 Err(e) => return Err(Error::Io(e)),
1449 }
1450 }
1451
1452 Ok((header_msg, BLK_SIZE))
1453}
1454
1455fn write_signed_gzip(
1456 out: &mut dyn Write,
1457 head: &[u8; 10],
1458 sig_header: &[u8],
1459 header_msg: &[u8],
1460 input: &mut std::fs::File,
1461 data_start: u64,
1462) -> Result<()> {
1463 let fake_header = [0x1f, 0x8b, 8, 16, 0, 0, 0, 0, head[8], 3];
1464 out.write_all(&fake_header).map_err(Error::Io)?;
1465 out.write_all(sig_header).map_err(Error::Io)?;
1466 out.write_all(header_msg).map_err(Error::Io)?;
1467 out.write_all(&[0u8]).map_err(Error::Io)?;
1468
1469 input.seek(SeekFrom::Start(data_start)).map_err(Error::Io)?;
1470 copy(input, out).map_err(Error::Io)?;
1471 Ok(())
1472}
1473
1474pub fn verify_gzip(
1476 pubkey: Option<&Path>,
1477 msg_path: &Path,
1478 sig_path: &Path,
1479 quiet: bool,
1480 msg_file: Option<File>,
1481 sig_file: Option<File>,
1482 pubkey_file: Option<File>,
1483) -> Result<()> {
1484 let mut sig_file: Box<dyn Read> = if let Some(f) = sig_file {
1485 Box::new(f)
1486 } else if sig_path.as_os_str() == "-" {
1487 Box::new(stdin())
1488 } else {
1489 Box::new(open(sig_path, false)?)
1490 };
1491
1492 let header = read_gzip_header(sig_file.as_mut())?;
1493
1494 let (sig, header_list) = parse_sig_from_comment(&header.comment_vec)?;
1496
1497 let pubkey = if let Some(f) = pubkey_file {
1499 let (pk, _) = parse_stream(f, PubKey::from_bytes)?;
1500 pk
1501 } else if let Some(path) = pubkey {
1502 let (pk, _) = parse::<PubKey, _>(path, PubKey::from_bytes)?;
1503 pk
1504 } else {
1505 return Err(Error::MissingPubKey);
1506 };
1507
1508 if sig.keynum != pubkey.keynum {
1509 return Err(Error::KeyMismatch);
1510 }
1511 crypto::verify(header_list, &pubkey.pubkey, &sig.sig)?;
1512
1513 let header_str = str::from_utf8(header_list).map_err(|_| Error::InvalidCommentHeader)?;
1515 let mut lines = header_str.lines();
1516 let blocksize = parse_header_metadata(&mut lines)?;
1517
1518 let mut out_writer: Option<Box<dyn Write>> = if let Some(f) = msg_file {
1520 Some(Box::new(f))
1521 } else if msg_path.as_os_str() == "-" {
1522 Some(Box::new(stdout()))
1523 } else {
1524 Some(Box::new(open(msg_path, true)?))
1525 };
1526
1527 if let Some(w) = out_writer.as_mut() {
1529 w.write_all(&header.head).map_err(Error::Io)?;
1530 if !header.extra_field.is_empty() {
1531 w.write_all(&header.extra_field).map_err(Error::Io)?;
1532 }
1533 if !header.name_field.is_empty() {
1534 w.write_all(&header.name_field).map_err(Error::Io)?;
1535 }
1536 w.write_all(&header.comment_vec).map_err(Error::Io)?;
1537 w.write_all(&[0u8]).map_err(Error::Io)?;
1538 }
1539
1540 if (header.flg & 2) != 0 {
1541 let mut crc = [0u8; 2];
1542 sig_file.read_exact(&mut crc).map_err(Error::Io)?;
1543 if let Some(w) = out_writer.as_mut() {
1544 w.write_all(&crc).map_err(Error::Io)?;
1545 }
1546 }
1547
1548 verify_payload_blocks(sig_file.as_mut(), lines, blocksize, out_writer.as_mut())?;
1549
1550 if !quiet {
1551 eprintln!("Signature Verified");
1552 }
1553 Ok(())
1554}
1555
1556fn parse_sig_from_comment(comment_vec: &[u8]) -> Result<(Sig, &[u8])> {
1557 let n1 = memchr(b'\n', comment_vec).ok_or(Error::InvalidCommentHeader)?;
1558 let n2_start = n1.checked_add(1).ok_or(Error::Overflow)?;
1559 let n2 = memchr(b'\n', &comment_vec[n2_start..]).ok_or(Error::MissingSignatureNewline)?;
1560 let sig_end = n2_start.checked_add(n2).ok_or(Error::Overflow)?;
1561
1562 let header_list = &comment_vec[sig_end.checked_add(1).ok_or(Error::Overflow)?..];
1563
1564 let b64_bytes = &comment_vec[n2_start..sig_end];
1565 let b64_str = str::from_utf8(b64_bytes).map_err(|_e| Error::InvalidSignatureUtf8)?;
1566 let sig_bytes = Base64::decode_vec(b64_str.trim()).map_err(Error::Base64Decode)?;
1567 let sig = Sig::from_bytes(&sig_bytes)?;
1568
1569 Ok((sig, header_list))
1570}
1571
1572fn parse_header_metadata(lines: &mut std::str::Lines) -> Result<usize> {
1573 let mut algo = "SHA256";
1574 let mut blocksize = 0x0001_0000;
1575
1576 for l in lines.by_ref() {
1577 if l.is_empty() {
1578 break;
1579 }
1580 if let Some(val) = l.strip_prefix("algorithm=") {
1581 algo = val;
1582 } else if let Some(val) = l.strip_prefix("blocksize=") {
1583 blocksize = val.parse().unwrap_or(0x0001_0000);
1584 }
1585 }
1586
1587 if algo != "SHA256" && algo != "SHA512/256" {
1588 return Err(Error::Io(std::io::Error::new(
1589 std::io::ErrorKind::InvalidData,
1590 format!("Unsupported algorithm: {algo}"),
1591 )));
1592 }
1593 Ok(blocksize)
1594}
1595
1596fn verify_payload_blocks(
1597 sig_file: &mut dyn Read,
1598 lines: std::str::Lines,
1599 blocksize: usize,
1600 mut out_writer: Option<&mut Box<dyn Write>>,
1601) -> Result<()> {
1602 let mut buf = vec![0u8; blocksize];
1603 for hash_line in lines {
1604 let n = loop {
1605 match sig_file.read(&mut buf) {
1606 Ok(n) => break n,
1607 Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1608 Err(e) => return Err(Error::Io(e)),
1609 }
1610 };
1611
1612 if n == 0 {
1613 return Err(Error::Io(std::io::Error::new(
1614 std::io::ErrorKind::UnexpectedEof,
1615 "Premature end of archive",
1616 )));
1617 }
1618 let hash = Sha256::digest(&buf[..n]);
1619 let hash_hex = HEXLOWER.encode(&hash);
1620
1621 if hash_hex != hash_line {
1622 return Err(Error::VerifyFailed);
1623 }
1624 if let Some(w) = out_writer.as_mut() {
1625 w.write_all(&buf[..n]).map_err(Error::Io)?;
1626 }
1627 }
1628
1629 let n = loop {
1631 match sig_file.read(&mut buf) {
1632 Ok(n) => break n,
1633 Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1634 Err(e) => return Err(Error::Io(e)),
1635 }
1636 };
1637 if n != 0 {
1638 return Err(Error::Io(std::io::Error::new(
1639 std::io::ErrorKind::InvalidData,
1640 "Trailing data in archive",
1641 )));
1642 }
1643 Ok(())
1644}
1645
1646fn make_sig_comment(seckey_path: &Path, comment_bytes: &[u8]) -> Result<Vec<u8>> {
1647 let mut sig_comment = Vec::new();
1648 if seckey_path.as_os_str() == "-" {
1649 sig_comment.extend_from_slice(b"signature from ");
1650 sig_comment.extend_from_slice(comment_bytes);
1651 } else {
1652 let basename = check_keyname_compliance(None, seckey_path)?;
1653 sig_comment.extend_from_slice(b"verify with ");
1654 sig_comment.extend_from_slice(basename.as_bytes());
1655 sig_comment.extend_from_slice(b".pub");
1656 };
1657 Ok(sig_comment)
1658}
1659
1660#[cfg(test)]
1661mod tests {
1662 use super::*;
1663 use std::path::PathBuf;
1664
1665 #[test]
1666 fn test_signer_default() {
1667 let signer = Signer::default();
1668 assert_eq!(signer.seckey, None);
1669 assert!(!signer.embed);
1670 assert!(!signer.gzip);
1671 assert_eq!(signer.key_id, None);
1672 }
1673
1674 #[test]
1675 fn test_signer_builder() {
1676 let path = PathBuf::from("test.sec");
1677 let signer = Signer::new()
1678 .seckey(path.clone())
1679 .embed(true)
1680 .gzip(true)
1681 .key_id(42);
1682
1683 assert_eq!(signer.seckey, Some(path));
1684 assert!(signer.embed);
1685 assert!(signer.gzip);
1686 assert_eq!(signer.key_id, Some(42));
1687 }
1688
1689 #[test]
1690 fn test_verifier_default() {
1691 let verifier = Verifier::default();
1692 assert_eq!(verifier.pubkey, None);
1693 assert!(!verifier.quiet);
1694 assert!(!verifier.embed);
1695 assert!(!verifier.gzip);
1696 }
1697
1698 #[test]
1699 fn test_verifier_builder() {
1700 let path = PathBuf::from("test.pub");
1701 let verifier = Verifier::new()
1702 .pubkey(path.clone())
1703 .quiet(true)
1704 .embed(true)
1705 .gzip(true);
1706
1707 assert_eq!(verifier.pubkey, Some(path));
1708 assert!(verifier.quiet);
1709 assert!(verifier.embed);
1710 assert!(verifier.gzip);
1711 }
1712}