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 if path.to_str() == Some("-") {
765 Some(Box::new(stdout()))
766 } else {
767 Some(Box::new(open(path, true)?))
768 }
769 } else {
770 None
771 };
772
773 let writer_ref = writer.as_mut().map(|w| &mut **w as &mut dyn Write);
775 crypto::verify_stream(stream, writer_ref, &pubkey.pubkey, &sig.sig)?;
776
777 if !self.quiet {
778 println!("Signature Verified");
779 }
780
781 Ok(())
782 }
783}
784
785fn parse_embedded_signature_reader(mut reader: Box<dyn Read>) -> EmbeddedSigResult {
786 const HEADER_LIMIT: usize = 4096;
788 let mut buffer = vec![0_u8; HEADER_LIMIT];
789 let mut valid_len = 0;
790
791 while valid_len < HEADER_LIMIT {
793 let n = reader.read(&mut buffer[valid_len..]).map_err(Error::Io)?;
794 if n == 0 {
795 break;
796 }
797 valid_len = valid_len.checked_add(n).ok_or(Error::Overflow)?;
798 }
799 buffer.truncate(valid_len);
800
801 let n1 = memchr(b'\n', &buffer).ok_or(Error::InvalidCommentHeader)?;
802 let n2_start = n1.checked_add(1).ok_or(Error::Overflow)?;
803 let n2 = memchr(b'\n', &buffer[n2_start..]).ok_or(Error::MissingSignatureNewline)?;
804 let b64_start = n2_start;
805 let b64_end = b64_start.checked_add(n2).ok_or(Error::Overflow)?;
806
807 if b64_end > buffer.len() {
808 return Err(Error::InvalidCommentHeader);
809 }
810
811 let b64_bytes = &buffer[b64_start..b64_end];
812 let b64_str = str::from_utf8(b64_bytes).map_err(|_e| Error::InvalidSignatureUtf8)?;
813 let sig_bytes = Base64::decode_vec(b64_str.trim()).map_err(Error::Base64Decode)?;
814 let sig = Sig::from_bytes(&sig_bytes)?;
815
816 let msg_start = b64_end.checked_add(1).ok_or(Error::Overflow)?;
817
818 let prelude = if msg_start < buffer.len() {
820 buffer[msg_start..].to_vec()
821 } else {
822 Vec::new()
823 };
824
825 Ok((sig, prelude, reader))
826}
827
828fn prompt_password(
830 confirm: bool,
831 key_id: Option<i32>,
832 tty: Option<&File>,
833) -> Result<Zeroizing<Vec<u8>>> {
834 let pass = read_password("passphrase: ", key_id, tty)?;
835
836 if confirm && key_id.is_none() {
837 eprint!("confirm passphrase: ");
838 stderr().flush().map_err(Error::Io)?;
839 let pass2 = read_password("passphrase: ", None, tty)?;
840 if pass != pass2 {
841 return Err(Error::PasswordMismatch);
842 }
843 }
844 Ok(pass)
845}
846
847struct SignParams<'a> {
849 seckey: &'a [u8; 64],
851 keynum: [u8; 8],
853 msg_path: &'a Path,
855 sig_path: &'a Path,
857 embed: bool,
859 is_stdout: bool,
861 is_stdin: bool,
863 sig_comment: &'a [u8],
865 msg_file: Option<File>,
867 sig_file: Option<File>,
869}
870
871fn sign_standard(params: SignParams) -> Result<()> {
872 let make_header = |sig_bytes: &[u8]| -> Result<Vec<u8>> {
874 #[expect(clippy::disallowed_methods)]
876 let sig = Sig {
877 pkalg: PKALG,
878 keynum: params.keynum,
879 sig: sig_bytes.try_into().unwrap(),
880 };
881 let encoded = Base64::encode_string(&sig.to_bytes());
882 let mut h = Vec::new();
883 h.extend_from_slice(COMMENTHDR.as_bytes());
884 h.extend_from_slice(params.sig_comment);
885 h.push(b'\n');
886 h.extend_from_slice(encoded.as_bytes());
887 h.push(b'\n');
888 Ok(h)
889 };
890
891 let out_file = if let Some(f) = params.sig_file {
892 Some(f)
893 } else if params.is_stdout {
894 None
895 } else {
896 Some(open(params.sig_path, true)?)
897 };
898
899 if params.embed {
900 if let Some(mut file) = out_file {
901 let dummy_sig = [0u8; 64];
904 let header = make_header(&dummy_sig)?;
905 file.write_all(&header).map_err(Error::Io)?;
906
907 let mut reader: Box<dyn Read> = if let Some(f) = params.msg_file {
909 Box::new(f)
910 } else if params.is_stdin {
911 Box::new(stdin())
912 } else {
913 Box::new(open(params.msg_path, false)?)
914 };
915
916 let mut buf = Vec::new();
917 reader.read_to_end(&mut buf).map_err(Error::Io)?;
918 file.write_all(&buf).map_err(Error::Io)?;
919
920 let real_sig = crypto::sign(&buf, params.seckey)?;
922 let real_header = make_header(real_sig.as_ref())?;
923 if real_header.len() != header.len() {
924 return Err(Error::InvalidSignatureLength);
925 }
926 file.rewind().map_err(Error::Io)?;
927 file.write_all(&real_header).map_err(Error::Io)?;
928 } else {
929 if params.is_stdin {
931 let mut buf = Vec::new();
932 stdin().read_to_end(&mut buf).map_err(Error::Io)?;
933
934 let sig = crypto::sign(&buf, params.seckey)?;
935 let header = make_header(sig.as_ref())?;
936
937 let mut out = stdout();
938 out.write_all(&header).map_err(Error::Io)?;
939 out.write_all(&buf).map_err(Error::Io)?;
940 } else {
941 let mut file = if let Some(file) = params.msg_file {
943 file
944 } else {
945 open(params.msg_path, false)?
946 };
947
948 let _ = file.rewind();
951
952 let sig = crypto::sign_stream(&mut file, params.seckey)?;
953 let header = make_header(&sig)?;
954 let mut out = stdout();
955 out.write_all(&header).map_err(Error::Io)?;
956
957 file.rewind().map_err(Error::Io)?;
959 copy(&mut file, &mut out).map_err(Error::Io)?;
960 }
961 }
962 } else {
963 let mut reader: Box<dyn Read> = if let Some(f) = params.msg_file {
965 Box::new(f)
966 } else if params.is_stdin {
967 Box::new(stdin())
968 } else {
969 Box::new(open(params.msg_path, false)?)
970 };
971 let sig = crypto::sign_stream(&mut reader, params.seckey)?;
972 let header = make_header(&sig)?;
973
974 if let Some(mut f) = out_file {
975 f.write_all(&header).map_err(Error::Io)?;
976 } else {
977 stdout().write_all(&header).map_err(Error::Io)?;
978 }
979 }
980
981 Ok(())
982}
983
984pub(crate) fn autolocate_key(comment: &[u8]) -> Result<PubKey> {
987 let marker = b"verify with ";
988 if let Some(idx) = memmem::find(comment, marker) {
989 let start = idx.checked_add(marker.len()).ok_or(Error::Overflow)?;
990 let keyname_slice = comment.get(start..).ok_or(Error::Overflow)?;
991 let keyname_bytes = keyname_slice.trim_ascii();
992 let keyname_str = str::from_utf8(keyname_bytes).map_err(|_e| Error::InvalidKeyName)?;
993 let keyname = Path::new(keyname_str);
994 let safepath = get_signify_dir();
995 let keypath = safepath.join(keyname);
996 let (pk, _) = parse::<PubKey, _>(&keypath, PubKey::from_bytes)
997 .map_err(|err| Error::AutolocateFailed(keypath.clone(), Box::new(err)))?;
998 Ok(pk)
999 } else {
1000 Err(Error::MissingPubKey)
1001 }
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: &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 (pubkey, _) = parse::<PubKey, _>(pubkey_path, PubKey::from_bytes)?;
1040
1041 if sig.keynum != pubkey.keynum {
1042 return Err(Error::KeyMismatch);
1043 }
1044
1045 crypto::verify(&msg, &pubkey.pubkey, &sig.sig)?;
1046
1047 let mut failed = false;
1048 for line in msg.split(|&b| b == b'\n') {
1049 let trimmed = line.trim_ascii();
1050 if trimmed.is_empty() {
1051 continue;
1052 }
1053
1054 if !verify_checksum_line(trimmed, quiet) {
1055 failed = true;
1056 }
1057 }
1058
1059 if failed {
1060 Err(Error::CheckFailed)
1061 } else {
1062 Ok(())
1063 }
1064}
1065
1066#[must_use]
1068pub fn verify_checksum_line(line: &[u8], quiet: bool) -> bool {
1069 let marker = b" = ";
1071 let Some(idx) = memmem::find(line, marker) else {
1072 return true;
1073 };
1074
1075 let left = &line[..idx];
1076 let Some(right_start) = idx.checked_add(marker.len()) else {
1077 return false;
1078 };
1079 let right = &line[right_start..];
1080 let hash_str = right.trim_ascii();
1081
1082 let Some(space_idx) = memchr::memchr(b' ', left) else {
1084 return true;
1085 };
1086 let algo = &left[..space_idx];
1087 let Some(rest_start) = space_idx.checked_add(1) else {
1088 return false;
1089 };
1090 let rest = &left[rest_start..];
1091
1092 if rest.len() < 2 || rest.first() != Some(&b'(') || rest.last() != Some(&b')') {
1094 return true;
1095 }
1096 let Some(filename_len) = rest.len().checked_sub(1) else {
1097 return false;
1098 };
1099
1100 let filename = match std::str::from_utf8(&rest[1..filename_len]) {
1101 Ok(filename) => filename,
1102 Err(error) => {
1103 println!("?: FAIL: {error}");
1104 return false;
1105 }
1106 };
1107 let filepath = Path::new(filename);
1108 let filename = log_untrusted_buf(filename.as_bytes());
1109
1110 if !filepath.exists() {
1111 println!("{filename}: FAIL");
1112 return false;
1113 }
1114
1115 const BUF_SIZE: usize = 64 * 1024;
1117 let mut buf = [0_u8; BUF_SIZE];
1118
1119 let calculated_hash = match algo {
1120 b"SHA256" => {
1121 let mut hasher = Sha256::new();
1122 if let Ok(file) = open(filepath, false) {
1123 let mut reader = BufReader::with_capacity(BUF_SIZE, file);
1124 loop {
1125 match reader.read(&mut buf) {
1126 Ok(0) => break,
1127 Ok(n) => hasher.update(&buf[..n]),
1128 Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1129 Err(_) => {
1130 println!("{filename}: FAIL");
1131 return false;
1132 }
1133 }
1134 }
1135 HEXLOWER.encode(&hasher.finalize())
1136 } else {
1137 println!("{filename}: FAIL");
1138 return false;
1139 }
1140 }
1141 b"SHA512" => {
1142 let mut hasher = Sha512::new();
1143 if let Ok(file) = open(filepath, false) {
1144 let mut reader = BufReader::with_capacity(BUF_SIZE, file);
1145 loop {
1146 match reader.read(&mut buf) {
1147 Ok(0) => break,
1148 Ok(n) => hasher.update(&buf[..n]),
1149 Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1150 Err(_) => {
1151 println!("{filename}: FAIL");
1152 return false;
1153 }
1154 }
1155 }
1156 HEXLOWER.encode(&hasher.finalize())
1157 } else {
1158 println!("{filename}: FAIL");
1159 return false;
1160 }
1161 }
1162 _ => {
1163 println!("{filename}: FAIL");
1164 return false;
1165 }
1166 };
1167
1168 if calculated_hash.as_bytes().eq_ignore_ascii_case(hash_str) {
1169 if !quiet {
1170 println!("{filename}: OK");
1171 }
1172 true
1173 } else {
1174 println!("{filename}: FAIL");
1175 false
1176 }
1177}
1178
1179fn read_gzip_header(sig_file: &mut dyn Read) -> Result<GzipHeader> {
1181 let mut head = [0u8; 10];
1182 sig_file.read_exact(&mut head).map_err(Error::Io)?;
1183
1184 if head[0] != 0x1f || head[1] != 0x8b {
1185 return Err(Error::Io(std::io::Error::new(
1186 std::io::ErrorKind::InvalidData,
1187 "Not a gzip file",
1188 )));
1189 }
1190 let flg = head[3];
1191 if (flg & 16) == 0 {
1192 return Err(Error::Io(std::io::Error::new(
1193 std::io::ErrorKind::InvalidData,
1194 "Unsigned gzip archive (no comment)",
1195 )));
1196 }
1197
1198 let mut extra_field = Vec::new();
1200 if (flg & 4) != 0 {
1201 let mut xlen_b = [0u8; 2];
1202 sig_file.read_exact(&mut xlen_b).map_err(Error::Io)?;
1203 extra_field.extend_from_slice(&xlen_b);
1204 let xlen = u64::from(u16::from_le_bytes(xlen_b));
1205 let mut reader = sig_file.take(xlen);
1206 reader.read_to_end(&mut extra_field).map_err(Error::Io)?;
1207 }
1208
1209 let mut name_field = Vec::new();
1210 if (flg & 8) != 0 {
1211 let mut buf = [0u8; 1];
1212 loop {
1213 sig_file.read_exact(&mut buf).map_err(Error::Io)?;
1214 name_field.push(buf[0]);
1215 if buf[0] == 0 {
1216 break;
1217 }
1218 }
1219 }
1220
1221 let mut comment_vec = Vec::new();
1223 let mut buf = [0u8; 1];
1224 loop {
1225 sig_file.read_exact(&mut buf).map_err(Error::Io)?;
1226 if buf[0] == 0 {
1227 break;
1228 }
1229 comment_vec.push(buf[0]);
1230 }
1231
1232 Ok(GzipHeader {
1233 flg,
1234 head,
1235 extra_field,
1236 name_field,
1237 comment_vec,
1238 })
1239}
1240
1241pub fn sign_gzip(
1243 seckey: &[u8],
1244 keynum: [u8; 8],
1245 seckey_path: &Path,
1246 msg_path: &Path,
1247 sig_path: &Path,
1248 comment_bytes: &[u8],
1249 msg_file: Option<std::fs::File>,
1250 sig_file: Option<std::fs::File>,
1251) -> Result<()> {
1252 let is_stdout = sig_path.as_os_str() == "-";
1253 let is_stdin = msg_path.as_os_str() == "-";
1254
1255 if is_stdin && msg_file.is_none() {
1256 return Err(Error::Io(std::io::Error::new(
1257 std::io::ErrorKind::InvalidInput,
1258 "Gzip signing requires a regular file input (not stdin)",
1259 )));
1260 }
1261
1262 let mut f = if let Some(f) = msg_file {
1263 f
1264 } else {
1265 open(msg_path, false)?
1266 };
1267
1268 let (head, data_start) = parse_gzip_for_signing(&mut f)?;
1269
1270 let (header_msg, _) = hash_gzip_content(&mut f, data_start, seckey_path)?;
1272
1273 let kp = ed25519_compact::KeyPair {
1275 pk: ed25519_compact::PublicKey::from_slice(&seckey[32..]).map_err(Error::Crypto)?,
1276 sk: ed25519_compact::SecretKey::from_slice(seckey).map_err(Error::Crypto)?,
1277 };
1278 let sig = kp.sk.sign(&header_msg, None);
1279
1280 let sig_comment = make_sig_comment(seckey_path, comment_bytes)?;
1281
1282 let sig_header = {
1283 let sig = Sig {
1284 pkalg: PKALG,
1285 keynum,
1286 sig: sig
1287 .as_ref()
1288 .try_into()
1289 .map_err(|_| Error::InvalidSignatureLength)?, };
1291 let encoded = Base64::encode_string(&sig.to_bytes());
1292 let mut h = Vec::new();
1293 h.extend_from_slice(COMMENTHDR.as_bytes());
1294 h.extend_from_slice(&sig_comment);
1295 h.push(b'\n');
1296 h.extend_from_slice(encoded.as_bytes());
1297 h.push(b'\n');
1298 h
1299 };
1300
1301 let mut out: Box<dyn Write> = if let Some(f) = sig_file {
1302 Box::new(f)
1303 } else if is_stdout {
1304 Box::new(stdout())
1305 } else {
1306 Box::new(open(sig_path, true)?)
1307 };
1308
1309 write_signed_gzip(
1310 &mut out,
1311 &head,
1312 &sig_header,
1313 &header_msg,
1314 &mut f,
1315 data_start,
1316 )
1317}
1318
1319fn parse_gzip_for_signing(f: &mut std::fs::File) -> Result<([u8; 10], u64)> {
1320 let mut head = [0u8; 10];
1321 f.read_exact(&mut head).map_err(Error::Io)?;
1322
1323 if head[0] != 0x1f || head[1] != 0x8b {
1324 return Err(Error::Io(std::io::Error::new(
1325 std::io::ErrorKind::InvalidData,
1326 "Not a gzip file",
1327 )));
1328 }
1329
1330 let flg = head[3];
1331 if (flg & 4) != 0 {
1332 let mut xlen_b = [0u8; 2];
1333 f.read_exact(&mut xlen_b).map_err(Error::Io)?;
1334 let xlen = i64::from(u16::from_le_bytes(xlen_b));
1335 f.seek(SeekFrom::Current(xlen)).map_err(Error::Io)?;
1336 }
1337
1338 if (flg & 8) != 0 {
1339 let mut buf = [0u8; 1];
1340 loop {
1341 f.read_exact(&mut buf).map_err(Error::Io)?;
1342 if buf[0] == 0 {
1343 break;
1344 }
1345 }
1346 }
1347
1348 if (flg & 16) != 0 {
1349 let mut buf = [0u8; 1];
1350 loop {
1351 f.read_exact(&mut buf).map_err(Error::Io)?;
1352 if buf[0] == 0 {
1353 break;
1354 }
1355 }
1356 }
1357
1358 if (flg & 2) != 0 {
1359 f.seek(SeekFrom::Current(2)).map_err(Error::Io)?;
1360 }
1361
1362 let data_start = f.stream_position().map_err(Error::Io)?;
1363 Ok((head, data_start))
1364}
1365
1366fn hash_gzip_content(
1367 f: &mut std::fs::File,
1368 data_start: u64,
1369 seckey_path: &Path,
1370) -> Result<(Vec<u8>, usize)> {
1371 const BLK_SIZE: usize = 0x0001_0000;
1372 let mut block = vec![0u8; BLK_SIZE];
1373
1374 loop {
1376 match f.read(&mut block) {
1377 Ok(0) => break,
1378 Ok(_) => continue,
1379 Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1380 Err(e) => return Err(Error::Io(e)),
1381 }
1382 }
1383
1384 let mut header_msg = Vec::new();
1385 let time_now = "0000-00-00T00:00:00Z";
1386 let keyname = if seckey_path.as_os_str() == "-" {
1387 "stdin"
1388 } else {
1389 seckey_path.to_str().ok_or(Error::InvalidPath)?
1390 };
1391 write!(
1392 &mut header_msg,
1393 "date={time_now}\nkey={keyname}\nalgorithm=SHA256\nblocksize={BLK_SIZE}\n\n",
1394 )
1395 .map_err(Error::Io)?;
1396
1397 f.seek(SeekFrom::Start(data_start)).map_err(Error::Io)?;
1398 loop {
1399 match f.read(&mut block) {
1400 Ok(0) => break,
1401 Ok(n) => {
1402 let hash = Sha256::digest(&block[..n]);
1403 let hex = HEXLOWER.encode(&hash);
1404 header_msg.extend_from_slice(hex.as_bytes());
1405 header_msg.push(b'\n');
1406 }
1407 Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1408 Err(e) => return Err(Error::Io(e)),
1409 }
1410 }
1411
1412 Ok((header_msg, BLK_SIZE))
1413}
1414
1415fn write_signed_gzip(
1416 out: &mut dyn Write,
1417 head: &[u8; 10],
1418 sig_header: &[u8],
1419 header_msg: &[u8],
1420 input: &mut std::fs::File,
1421 data_start: u64,
1422) -> Result<()> {
1423 let fake_header = [0x1f, 0x8b, 8, 16, 0, 0, 0, 0, head[8], 3];
1424 out.write_all(&fake_header).map_err(Error::Io)?;
1425 out.write_all(sig_header).map_err(Error::Io)?;
1426 out.write_all(header_msg).map_err(Error::Io)?;
1427 out.write_all(&[0u8]).map_err(Error::Io)?;
1428
1429 input.seek(SeekFrom::Start(data_start)).map_err(Error::Io)?;
1430 copy(input, out).map_err(Error::Io)?;
1431 Ok(())
1432}
1433
1434pub fn verify_gzip(
1436 pubkey: Option<&Path>,
1437 msg_path: &Path,
1438 sig_path: &Path,
1439 quiet: bool,
1440 msg_file: Option<File>,
1441 sig_file: Option<File>,
1442 pubkey_file: Option<File>,
1443) -> Result<()> {
1444 let mut sig_file: Box<dyn Read> = if let Some(f) = sig_file {
1445 Box::new(f)
1446 } else if sig_path.as_os_str() == "-" {
1447 Box::new(stdin())
1448 } else {
1449 Box::new(open(sig_path, false)?)
1450 };
1451
1452 let header = read_gzip_header(sig_file.as_mut())?;
1453
1454 let (sig, header_list) = parse_sig_from_comment(&header.comment_vec)?;
1456
1457 let pubkey = if let Some(f) = pubkey_file {
1459 let (pk, _) = parse_stream(f, PubKey::from_bytes)?;
1460 pk
1461 } else if let Some(path) = pubkey {
1462 let (pk, _) = parse::<PubKey, _>(path, PubKey::from_bytes)?;
1463 pk
1464 } else {
1465 autolocate_key(&header.comment_vec)?
1466 };
1467
1468 if sig.keynum != pubkey.keynum {
1469 return Err(Error::KeyMismatch);
1470 }
1471 crypto::verify(header_list, &pubkey.pubkey, &sig.sig)?;
1472
1473 let header_str = str::from_utf8(header_list).map_err(|_| Error::InvalidCommentHeader)?;
1475 let mut lines = header_str.lines();
1476 let blocksize = parse_header_metadata(&mut lines)?;
1477
1478 let mut out_writer: Option<Box<dyn Write>> = if let Some(f) = msg_file {
1480 Some(Box::new(f))
1481 } else if msg_path.as_os_str() == "-" {
1482 Some(Box::new(stdout()))
1483 } else {
1484 Some(Box::new(open(msg_path, true)?))
1485 };
1486
1487 if let Some(w) = out_writer.as_mut() {
1489 w.write_all(&header.head).map_err(Error::Io)?;
1490 if !header.extra_field.is_empty() {
1491 w.write_all(&header.extra_field).map_err(Error::Io)?;
1492 }
1493 if !header.name_field.is_empty() {
1494 w.write_all(&header.name_field).map_err(Error::Io)?;
1495 }
1496 w.write_all(&header.comment_vec).map_err(Error::Io)?;
1497 w.write_all(&[0u8]).map_err(Error::Io)?;
1498 }
1499
1500 if (header.flg & 2) != 0 {
1501 let mut crc = [0u8; 2];
1502 sig_file.read_exact(&mut crc).map_err(Error::Io)?;
1503 if let Some(w) = out_writer.as_mut() {
1504 w.write_all(&crc).map_err(Error::Io)?;
1505 }
1506 }
1507
1508 verify_payload_blocks(sig_file.as_mut(), lines, blocksize, out_writer.as_mut())?;
1509
1510 if !quiet {
1511 eprintln!("Signature Verified");
1512 }
1513 Ok(())
1514}
1515
1516fn parse_sig_from_comment(comment_vec: &[u8]) -> Result<(Sig, &[u8])> {
1517 let n1 = memchr(b'\n', comment_vec).ok_or(Error::InvalidCommentHeader)?;
1518 let n2_start = n1.checked_add(1).ok_or(Error::Overflow)?;
1519 let n2 = memchr(b'\n', &comment_vec[n2_start..]).ok_or(Error::MissingSignatureNewline)?;
1520 let sig_end = n2_start.checked_add(n2).ok_or(Error::Overflow)?;
1521
1522 let header_list = &comment_vec[sig_end.checked_add(1).ok_or(Error::Overflow)?..];
1523
1524 let b64_bytes = &comment_vec[n2_start..sig_end];
1525 let b64_str = str::from_utf8(b64_bytes).map_err(|_e| Error::InvalidSignatureUtf8)?;
1526 let sig_bytes = Base64::decode_vec(b64_str.trim()).map_err(Error::Base64Decode)?;
1527 let sig = Sig::from_bytes(&sig_bytes)?;
1528
1529 Ok((sig, header_list))
1530}
1531
1532fn parse_header_metadata(lines: &mut std::str::Lines) -> Result<usize> {
1533 let mut algo = "SHA256";
1534 let mut blocksize = 0x0001_0000;
1535
1536 for l in lines.by_ref() {
1537 if l.is_empty() {
1538 break;
1539 }
1540 if let Some(val) = l.strip_prefix("algorithm=") {
1541 algo = val;
1542 } else if let Some(val) = l.strip_prefix("blocksize=") {
1543 blocksize = val.parse().unwrap_or(0x0001_0000);
1544 }
1545 }
1546
1547 if algo != "SHA256" && algo != "SHA512/256" {
1548 return Err(Error::Io(std::io::Error::new(
1549 std::io::ErrorKind::InvalidData,
1550 format!("Unsupported algorithm: {algo}"),
1551 )));
1552 }
1553 Ok(blocksize)
1554}
1555
1556fn verify_payload_blocks(
1557 sig_file: &mut dyn Read,
1558 lines: std::str::Lines,
1559 blocksize: usize,
1560 mut out_writer: Option<&mut Box<dyn Write>>,
1561) -> Result<()> {
1562 let mut buf = vec![0u8; blocksize];
1563 for hash_line in lines {
1564 let n = loop {
1565 match sig_file.read(&mut buf) {
1566 Ok(n) => break n,
1567 Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1568 Err(e) => return Err(Error::Io(e)),
1569 }
1570 };
1571
1572 if n == 0 {
1573 return Err(Error::Io(std::io::Error::new(
1574 std::io::ErrorKind::UnexpectedEof,
1575 "Premature end of archive",
1576 )));
1577 }
1578 let hash = Sha256::digest(&buf[..n]);
1579 let hash_hex = HEXLOWER.encode(&hash);
1580
1581 if hash_hex != hash_line {
1582 return Err(Error::VerifyFailed);
1583 }
1584 if let Some(w) = out_writer.as_mut() {
1585 w.write_all(&buf[..n]).map_err(Error::Io)?;
1586 }
1587 }
1588
1589 let n = loop {
1591 match sig_file.read(&mut buf) {
1592 Ok(n) => break n,
1593 Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1594 Err(e) => return Err(Error::Io(e)),
1595 }
1596 };
1597 if n != 0 {
1598 return Err(Error::Io(std::io::Error::new(
1599 std::io::ErrorKind::InvalidData,
1600 "Trailing data in archive",
1601 )));
1602 }
1603 Ok(())
1604}
1605
1606fn make_sig_comment(seckey_path: &Path, comment_bytes: &[u8]) -> Result<Vec<u8>> {
1607 let mut sig_comment = Vec::new();
1608 if seckey_path.as_os_str() == "-" {
1609 sig_comment.extend_from_slice(b"signature from ");
1610 sig_comment.extend_from_slice(comment_bytes);
1611 } else {
1612 let basename = check_keyname_compliance(None, seckey_path)?;
1613 sig_comment.extend_from_slice(b"verify with ");
1614 sig_comment.extend_from_slice(basename.as_bytes());
1615 sig_comment.extend_from_slice(b".pub");
1616 };
1617 Ok(sig_comment)
1618}
1619
1620#[cfg(test)]
1621mod tests {
1622 use super::*;
1623 use std::path::PathBuf;
1624
1625 #[test]
1626 fn test_signer_default() {
1627 let signer = Signer::default();
1628 assert_eq!(signer.seckey, None);
1629 assert!(!signer.embed);
1630 assert!(!signer.gzip);
1631 assert_eq!(signer.key_id, None);
1632 }
1633
1634 #[test]
1635 fn test_signer_builder() {
1636 let path = PathBuf::from("test.sec");
1637 let signer = Signer::new()
1638 .seckey(path.clone())
1639 .embed(true)
1640 .gzip(true)
1641 .key_id(42);
1642
1643 assert_eq!(signer.seckey, Some(path));
1644 assert!(signer.embed);
1645 assert!(signer.gzip);
1646 assert_eq!(signer.key_id, Some(42));
1647 }
1648
1649 #[test]
1650 fn test_verifier_default() {
1651 let verifier = Verifier::default();
1652 assert_eq!(verifier.pubkey, None);
1653 assert!(!verifier.quiet);
1654 assert!(!verifier.embed);
1655 assert!(!verifier.gzip);
1656 }
1657
1658 #[test]
1659 fn test_verifier_builder() {
1660 let path = PathBuf::from("test.pub");
1661 let verifier = Verifier::new()
1662 .pubkey(path.clone())
1663 .quiet(true)
1664 .embed(true)
1665 .gzip(true);
1666
1667 assert_eq!(verifier.pubkey, Some(path));
1668 assert!(verifier.quiet);
1669 assert!(verifier.embed);
1670 assert!(verifier.gzip);
1671 }
1672}