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