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::{parse, write, EncKey, PubKey, Sig};
21use crate::utils::log_untrusted_buf;
22use crate::utils::read_password;
23use crate::utils::{check_keyname_compliance, get_signify_dir};
24use base64ct::{Base64, Encoding as _};
25use data_encoding::HEXLOWER;
26use memchr::memchr;
27use memchr::memmem;
28use rand_core::{OsRng, TryRngCore as _};
29use sha2::{Digest as _, Sha256, Sha512};
30use std::fs::OpenOptions;
31use std::io::stdout;
32use std::io::BufReader;
33use std::io::Cursor;
34use std::io::{copy, stderr, stdin};
35use std::io::{Read, Seek, SeekFrom, Write};
36use std::path::{Path, PathBuf};
37use std::str;
38use zeroize::Zeroizing;
39
40type EmbeddedSigResult = Result<(Sig, Vec<u8>, Box<dyn Read>)>;
41
42struct GzipHeader {
44 flg: u8,
46 head: [u8; 10],
48 extra_field: Vec<u8>,
50 name_field: Vec<u8>,
52 comment_vec: Vec<u8>,
54}
55
56#[derive(Default)]
81pub struct KeyGenerator {
82 rounds: u32,
83 comment: Option<String>,
84 key_id: Option<i32>,
85}
86
87impl KeyGenerator {
88 #[must_use]
90 pub fn new() -> Self {
91 Self {
92 rounds: crypto::DEFAULT_ROUNDS,
93 comment: None,
94 key_id: None,
95 }
96 }
97
98 #[must_use]
102 pub fn rounds(mut self, rounds: u32) -> Self {
103 self.rounds = rounds;
104 self
105 }
106
107 #[must_use]
111 pub fn comment(mut self, comment: impl Into<String>) -> Self {
112 self.comment = Some(comment.into());
113 self
114 }
115
116 #[must_use]
118 pub fn key_id(mut self, key_id: i32) -> Self {
119 self.key_id = Some(key_id);
120 self
121 }
122
123 pub fn generate(self, pubkey_path: &Path, seckey_path: &Path) -> Result<()> {
128 let comment = self.comment.as_deref().unwrap_or("signify key");
129
130 let (public_key, secret_key) = generate_keypair();
131
132 let mut keynum = [0_u8; KEYNUMLEN];
133 OsRng.try_fill_bytes(&mut keynum)?;
134
135 let mut salt = [0_u8; SALT_LEN];
136 OsRng.try_fill_bytes(&mut salt)?;
137
138 let xorkey = if self.rounds > 0 {
139 let pass = prompt_password(true, self.key_id)?;
140 let mut out = Zeroizing::new(vec![0u8; SECKEY_LEN]);
141 kdf(&pass, &salt, self.rounds, &mut out)?;
142 out
143 } else {
144 Zeroizing::new(vec![0u8; SECKEY_LEN])
145 };
146
147 let checksum = derive_checksum(&secret_key);
148
149 let mut xored_seckey = [0_u8; SECKEY_LEN];
150 for i in 0..SECKEY_LEN {
151 xored_seckey[i] = secret_key[i] ^ xorkey[i];
152 }
153
154 let enc_key = EncKey {
155 pkalg: PKALG,
156 kdfalg: KDFALG,
157 kdfrounds: self.rounds,
158 salt,
159 checksum,
160 keynum,
161 seckey: xored_seckey,
162 };
163
164 let pub_key = PubKey {
165 pkalg: PKALG,
166 keynum,
167 pubkey: public_key,
168 };
169
170 let comment_len = comment.len().checked_add(11).ok_or(Error::Overflow)?;
171 let mut seckey_comment = Vec::with_capacity(comment_len);
172 seckey_comment.extend_from_slice(comment.as_bytes());
173 seckey_comment.extend_from_slice(b" secret key");
174 write(seckey_path, &seckey_comment, &enc_key.to_bytes())?;
175
176 let mut pubkey_comment = Vec::with_capacity(comment_len);
177 pubkey_comment.extend_from_slice(comment.as_bytes());
178 pubkey_comment.extend_from_slice(b" public key");
179 write(pubkey_path, &pubkey_comment, &pub_key.to_bytes())
180 }
181}
182
183pub struct Signer {
269 seckey: Option<PathBuf>,
270 embed: bool,
271 gzip: bool,
272 key_id: Option<i32>,
273}
274
275impl Default for Signer {
276 fn default() -> Self {
277 Self::new()
278 }
279}
280
281impl Signer {
282 #[must_use]
284 pub fn new() -> Self {
285 Self {
286 seckey: None,
287 embed: false,
288 gzip: false,
289 key_id: None,
290 }
291 }
292
293 #[must_use]
295 pub fn seckey(mut self, path: impl Into<PathBuf>) -> Self {
296 self.seckey = Some(path.into());
297 self
298 }
299
300 #[must_use]
304 pub fn embed(mut self, embed: bool) -> Self {
305 self.embed = embed;
306 self
307 }
308
309 #[must_use]
313 pub fn gzip(mut self, gzip: bool) -> Self {
314 self.gzip = gzip;
315 self
316 }
317
318 #[must_use]
320 pub fn key_id(mut self, key_id: i32) -> Self {
321 self.key_id = Some(key_id);
322 self
323 }
324
325 pub fn sign(self, msg_path: &Path, sig_path: &Path) -> Result<()> {
330 let seckey_path = self.seckey.as_deref().ok_or(Error::RequiredArg("-s"))?;
331
332 let (enc_key, comment_bytes) = parse::<EncKey, _>(seckey_path, EncKey::from_bytes)?;
333
334 let xorkey = if enc_key.kdfrounds > 0 {
335 let pass = prompt_password(false, self.key_id)?;
336 let mut out = Zeroizing::new(vec![0u8; SECKEY_LEN]);
337 kdf(&pass, &enc_key.salt, enc_key.kdfrounds, &mut out)?;
338 out
339 } else {
340 Zeroizing::new(vec![0u8; SECKEY_LEN])
341 };
342
343 let mut seckey = [0_u8; SECKEY_LEN];
344 for i in 0..SECKEY_LEN {
345 seckey[i] = enc_key.seckey[i] ^ xorkey[i];
346 }
347
348 let checksum = derive_checksum(&seckey);
349 if checksum != enc_key.checksum {
350 return Err(Error::IncorrectPassphrase);
351 }
352
353 if self.gzip {
354 if self.embed {
355 return Err(Error::Io(std::io::Error::new(
356 std::io::ErrorKind::InvalidInput,
357 "cannot combine -e (embed) and -z (gzip)",
358 )));
359 }
360 return sign_gzip(
361 &seckey,
362 enc_key.keynum,
363 seckey_path,
364 msg_path,
365 sig_path,
366 &comment_bytes,
367 );
368 }
369
370 let is_stdout = sig_path.to_str() == Some("-");
372 let is_stdin = msg_path.to_str() == Some("-");
373
374 let mut sig_comment = Vec::new();
376 if seckey_path.to_str() == Some("-") {
377 sig_comment.extend_from_slice(b"signature from ");
378 sig_comment.extend_from_slice(&comment_bytes);
379 } else {
380 let basename = check_keyname_compliance(None, seckey_path)?;
381 sig_comment.extend_from_slice(b"verify with ");
382 sig_comment.extend_from_slice(basename.as_bytes());
383 sig_comment.extend_from_slice(b".pub");
384 };
385
386 sign_standard(SignParams {
387 seckey: &seckey,
388 keynum: enc_key.keynum,
389 msg_path,
390 sig_path,
391 embed: self.embed,
392 is_stdout,
393 is_stdin,
394 sig_comment: &sig_comment,
395 })
396 }
397}
398
399pub struct Verifier {
529 pubkey: Option<PathBuf>,
530 quiet: bool,
531 embed: bool,
532 gzip: bool,
533}
534
535impl Default for Verifier {
536 fn default() -> Self {
537 Self::new()
538 }
539}
540
541impl Verifier {
542 #[must_use]
544 pub fn new() -> Self {
545 Self {
546 pubkey: None,
547 quiet: false,
548 embed: false,
549 gzip: false,
550 }
551 }
552
553 #[must_use]
555 pub fn pubkey(mut self, path: impl Into<PathBuf>) -> Self {
556 self.pubkey = Some(path.into());
557 self
558 }
559
560 #[must_use]
564 pub fn quiet(mut self, quiet: bool) -> Self {
565 self.quiet = quiet;
566 self
567 }
568
569 #[must_use]
573 pub fn embed(mut self, embed: bool) -> Self {
574 self.embed = embed;
575 self
576 }
577
578 #[must_use]
582 pub fn gzip(mut self, gzip: bool) -> Self {
583 self.gzip = gzip;
584 self
585 }
586
587 pub fn verify(self, msg_path: &Path, sig_path: &Path) -> Result<()> {
592 if self.gzip {
593 return verify_gzip(self.pubkey.as_deref(), msg_path, sig_path, self.quiet);
594 }
595
596 let (signature, stream, output_path, comment_opt) = if self.embed {
599 let (sig_obj, prelude, rest_reader) = parse_embedded_signature(sig_path)?;
600 let stream = Cursor::new(prelude).chain(rest_reader);
601
602 let out_path = if self.embed { Some(msg_path) } else { None };
603 (sig_obj, Box::new(stream) as Box<dyn Read>, out_path, None)
604 } else {
605 let (sig_obj, comment_content) = parse::<Sig, _>(sig_path, Sig::from_bytes)?;
607 let file = OpenOptions::new()
608 .read(true)
609 .open(msg_path)
610 .map_err(Error::Io)?;
611
612 (
614 sig_obj,
615 Box::new(file) as Box<dyn Read>,
616 None,
617 Some(comment_content),
618 )
619 };
620
621 let pub_key_obj = if let Some(path) = &self.pubkey {
622 let (pk, _) = parse::<PubKey, _>(path, PubKey::from_bytes)?;
623 pk
624 } else {
625 let comment = if let Some(comment_bytes) = comment_opt {
627 comment_bytes
628 } else {
629 let mut f = OpenOptions::new()
632 .read(true)
633 .open(sig_path)
634 .map_err(Error::Io)?;
635 let mut buf = vec![0u8; 4096];
636 let n = f.read(&mut buf).map_err(Error::Io)?;
637 let nl = memchr(b'\n', &buf[..n]).ok_or(Error::InvalidCommentHeader)?;
638 buf[..nl].to_vec()
639 };
640 autolocate_key(&comment)?
641 };
642
643 if signature.keynum != pub_key_obj.keynum {
644 return Err(Error::KeyMismatch);
645 }
646
647 let mut writer: Option<Box<dyn Write>> = if let Some(path) = output_path {
649 let f = OpenOptions::new()
650 .write(true)
651 .create_new(true)
652 .open(path)
653 .map_err(Error::Io)?;
654 Some(Box::new(f))
655 } else {
656 None
657 };
658
659 let writer_ref = writer.as_mut().map(|w| &mut **w as &mut dyn Write);
661 crypto::verify_stream(stream, writer_ref, &pub_key_obj.pubkey, &signature.sig)?;
662
663 if !self.quiet {
664 println!("Signature Verified");
665 }
666
667 Ok(())
668 }
669}
670
671fn prompt_password(confirm: bool, key_id: Option<i32>) -> Result<zeroize::Zeroizing<Vec<u8>>> {
673 let pass = read_password("passphrase: ", key_id)?;
674
675 if confirm && key_id.is_none() {
676 eprint!("confirm passphrase: ");
677 stderr().flush().map_err(Error::Io)?;
678 let pass2 = read_password("passphrase: ", None)?;
679 if pass != pass2 {
680 return Err(Error::PasswordMismatch);
681 }
682 }
683 Ok(pass)
684}
685
686struct SignParams<'a> {
688 seckey: &'a [u8; 64],
690 keynum: [u8; 8],
692 msg_path: &'a Path,
694 sig_path: &'a Path,
696 embed: bool,
698 is_stdout: bool,
700 is_stdin: bool,
702 sig_comment: &'a [u8],
704}
705
706fn sign_standard(params: SignParams) -> Result<()> {
707 let make_header = |sig_bytes: &[u8]| -> Result<Vec<u8>> {
709 #[expect(clippy::disallowed_methods)]
711 let sig_obj = Sig {
712 pkalg: PKALG,
713 keynum: params.keynum,
714 sig: sig_bytes.try_into().unwrap(),
715 };
716 let encoded = Base64::encode_string(&sig_obj.to_bytes());
717 let mut h = Vec::new();
718 h.extend_from_slice(COMMENTHDR.as_bytes());
719 h.extend_from_slice(params.sig_comment);
720 h.push(b'\n');
721 h.extend_from_slice(encoded.as_bytes());
722 h.push(b'\n');
723 Ok(h)
724 };
725
726 let out_file = if params.is_stdout {
727 None
728 } else {
729 Some(
730 OpenOptions::new()
731 .write(true)
732 .create_new(true)
733 .open(params.sig_path)
734 .map_err(Error::Io)?,
735 )
736 };
737
738 if params.embed {
739 if let Some(mut file) = out_file {
740 let dummy_sig = [0u8; 64];
743 let header = make_header(&dummy_sig)?;
744 file.write_all(&header).map_err(Error::Io)?;
745
746 let mut reader: Box<dyn Read> = if params.is_stdin {
748 Box::new(stdin())
749 } else {
750 Box::new(
751 OpenOptions::new()
752 .read(true)
753 .open(params.msg_path)
754 .map_err(Error::Io)?,
755 )
756 };
757
758 let kp = ed25519_compact::KeyPair {
759 pk: ed25519_compact::PublicKey::from_slice(¶ms.seckey[32..])
760 .map_err(Error::Crypto)?,
761 sk: ed25519_compact::SecretKey::from_slice(params.seckey).map_err(Error::Crypto)?,
762 };
763 let mut ctx = kp.sk.sign_incremental(ed25519_compact::Noise::default());
764
765 let mut buf = [0u8; 64 * 1024];
766 loop {
767 let n = reader.read(&mut buf).map_err(Error::Io)?;
768 if n == 0 {
769 break;
770 }
771 ctx.absorb(&buf[..n]);
772 file.write_all(&buf[..n]).map_err(Error::Io)?;
773 }
774
775 let real_sig = ctx.sign();
777 let real_header = make_header(real_sig.as_ref())?;
778 if real_header.len() != header.len() {
779 return Err(Error::InvalidSignatureLength);
780 }
781 file.seek(SeekFrom::Start(0)).map_err(Error::Io)?;
782 file.write_all(&real_header).map_err(Error::Io)?;
783 } else {
784 if params.is_stdin {
786 return Err(Error::Io(std::io::Error::new(
788 std::io::ErrorKind::InvalidInput,
789 "Cannot embedded-sign stdin to stdout",
790 )));
791 } else {
792 let mut f = OpenOptions::new()
794 .read(true)
795 .open(params.msg_path)
796 .map_err(Error::Io)?;
797 let sig = crypto::sign_stream(&mut f, params.seckey)?;
798 let header = make_header(&sig)?;
799 let mut out = stdout();
800 out.write_all(&header).map_err(Error::Io)?;
801
802 let mut f = OpenOptions::new()
803 .read(true)
804 .open(params.msg_path)
805 .map_err(Error::Io)?;
806 copy(&mut f, &mut out).map_err(Error::Io)?;
807 }
808 }
809 } else {
810 let mut reader: Box<dyn Read> = if params.is_stdin {
812 Box::new(stdin())
813 } else {
814 Box::new(
815 OpenOptions::new()
816 .read(true)
817 .open(params.msg_path)
818 .map_err(Error::Io)?,
819 )
820 };
821 let sig = crypto::sign_stream(&mut reader, params.seckey)?;
822 let header = make_header(&sig)?;
823
824 if let Some(mut f) = out_file {
825 f.write_all(&header).map_err(Error::Io)?;
826 } else {
827 stdout().write_all(&header).map_err(Error::Io)?;
828 }
829 }
830
831 Ok(())
832}
833
834pub(crate) fn autolocate_key(comment: &[u8]) -> Result<PubKey> {
837 let marker = b"verify with ";
838 if let Some(idx) = memmem::find(comment, marker) {
839 let start = idx.checked_add(marker.len()).ok_or(Error::Overflow)?;
840 let keyname_slice = comment.get(start..).ok_or(Error::Overflow)?;
841 let keyname_bytes = keyname_slice.trim_ascii();
842 let keyname_str = str::from_utf8(keyname_bytes).map_err(|_e| Error::InvalidKeyName)?;
843 let keyname = Path::new(keyname_str);
844 let safepath = get_signify_dir();
845 let keypath = safepath.join(keyname);
846 let (pk, _) = parse::<PubKey, _>(&keypath, PubKey::from_bytes)
847 .map_err(|err| Error::AutolocateFailed(keypath.clone(), Box::new(err)))?;
848 Ok(pk)
849 } else {
850 Err(Error::MissingPubKey)
851 }
852}
853
854fn parse_embedded_signature(path: &Path) -> EmbeddedSigResult {
859 let mut reader: Box<dyn Read> = if path.to_str() == Some("-") {
860 Box::new(stdin())
861 } else {
862 let file = OpenOptions::new()
863 .read(true)
864 .open(path)
865 .map_err(Error::Io)?;
866 Box::new(file)
867 };
868
869 const HEADER_LIMIT: usize = 4096;
871 let mut buffer = vec![0_u8; HEADER_LIMIT];
872 let mut valid_len = 0;
873
874 while valid_len < HEADER_LIMIT {
876 let n = reader.read(&mut buffer[valid_len..]).map_err(Error::Io)?;
877 if n == 0 {
878 break;
879 }
880 valid_len = valid_len.checked_add(n).ok_or(Error::Overflow)?;
881 }
882 buffer.truncate(valid_len);
883
884 let n1 = memchr(b'\n', &buffer).ok_or(Error::InvalidCommentHeader)?;
885 let n2_start = n1.checked_add(1).ok_or(Error::Overflow)?;
886 let n2 = memchr(b'\n', &buffer[n2_start..]).ok_or(Error::MissingSignatureNewline)?;
887 let b64_start = n2_start;
888 let b64_end = b64_start.checked_add(n2).ok_or(Error::Overflow)?;
889
890 if b64_end > buffer.len() {
891 return Err(Error::InvalidCommentHeader);
892 }
893
894 let b64_bytes = &buffer[b64_start..b64_end];
895 let b64_str = str::from_utf8(b64_bytes).map_err(|_e| Error::InvalidSignatureUtf8)?;
896 let sig_bytes = Base64::decode_vec(b64_str.trim()).map_err(Error::Base64Decode)?;
897 let sig_obj = Sig::from_bytes(&sig_bytes)?;
898
899 let msg_start = b64_end.checked_add(1).ok_or(Error::Overflow)?;
900
901 let prelude = if msg_start < buffer.len() {
903 buffer[msg_start..].to_vec()
904 } else {
905 Vec::new()
906 };
907
908 Ok((sig_obj, prelude, reader))
909}
910
911pub fn check_checksums(pubkey_path: &Path, sig_path: &Path, quiet: bool) -> Result<()> {
920 let (sig, mut prelude, mut reader) = parse_embedded_signature(sig_path)?;
921 reader.read_to_end(&mut prelude).map_err(Error::Io)?;
922 let msg = prelude;
923
924 let (pub_key_obj, _) = parse::<PubKey, _>(pubkey_path, PubKey::from_bytes)?;
925
926 if sig.keynum != pub_key_obj.keynum {
927 return Err(Error::KeyMismatch);
928 }
929
930 crypto::verify(&msg, &pub_key_obj.pubkey, &sig.sig)?;
931
932 let mut failed = false;
933 for line in msg.split(|&b| b == b'\n') {
934 let trimmed = line.trim_ascii();
935 if trimmed.is_empty() {
936 continue;
937 }
938
939 if !verify_checksum_line(trimmed, quiet) {
940 failed = true;
941 }
942 }
943
944 if failed {
945 Err(Error::CheckFailed)
946 } else {
947 Ok(())
948 }
949}
950
951#[must_use]
953pub fn verify_checksum_line(line: &[u8], quiet: bool) -> bool {
954 let marker = b" = ";
956 let Some(idx) = memmem::find(line, marker) else {
957 return true;
958 };
959
960 let left = &line[..idx];
961 let Some(right_start) = idx.checked_add(marker.len()) else {
962 return false;
963 };
964 let right = &line[right_start..];
965 let hash_str = right.trim_ascii();
966
967 let Some(space_idx) = memchr::memchr(b' ', left) else {
969 return true;
970 };
971 let algo = &left[..space_idx];
972 let Some(rest_start) = space_idx.checked_add(1) else {
973 return false;
974 };
975 let rest = &left[rest_start..];
976
977 if rest.len() < 2 || rest.first() != Some(&b'(') || rest.last() != Some(&b')') {
979 return true;
980 }
981 let Some(filename_len) = rest.len().checked_sub(1) else {
982 return false;
983 };
984
985 let filename = match std::str::from_utf8(&rest[1..filename_len]) {
986 Ok(filename) => filename,
987 Err(error) => {
988 println!("?: FAIL: {error}");
989 return false;
990 }
991 };
992 let filepath = Path::new(filename);
993 let filename = log_untrusted_buf(filename.as_bytes());
994
995 if !filepath.exists() {
996 println!("{filename}: FAIL");
997 return false;
998 }
999
1000 const BUF_SIZE: usize = 64 * 1024;
1002 let mut buf = [0_u8; BUF_SIZE];
1003
1004 let calculated_hash = match algo {
1005 b"SHA256" => {
1006 let mut hasher = Sha256::new();
1007 if let Ok(file) = OpenOptions::new().read(true).open(filepath) {
1008 let mut reader = BufReader::with_capacity(BUF_SIZE, file);
1009 loop {
1010 match reader.read(&mut buf) {
1011 Ok(0) => break,
1012 Ok(n) => hasher.update(&buf[..n]),
1013 Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1014 Err(_) => {
1015 println!("{filename}: FAIL");
1016 return false;
1017 }
1018 }
1019 }
1020 HEXLOWER.encode(&hasher.finalize())
1021 } else {
1022 println!("{filename}: FAIL");
1023 return false;
1024 }
1025 }
1026 b"SHA512" => {
1027 let mut hasher = Sha512::new();
1028 if let Ok(file) = OpenOptions::new().read(true).open(filepath) {
1029 let mut reader = BufReader::with_capacity(BUF_SIZE, file);
1030 loop {
1031 match reader.read(&mut buf) {
1032 Ok(0) => break,
1033 Ok(n) => hasher.update(&buf[..n]),
1034 Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1035 Err(_) => {
1036 println!("{filename}: FAIL");
1037 return false;
1038 }
1039 }
1040 }
1041 HEXLOWER.encode(&hasher.finalize())
1042 } else {
1043 println!("{filename}: FAIL");
1044 return false;
1045 }
1046 }
1047 _ => {
1048 println!("{filename}: FAIL");
1049 return false;
1050 }
1051 };
1052
1053 if calculated_hash.as_bytes().eq_ignore_ascii_case(hash_str) {
1054 if !quiet {
1055 println!("{filename}: OK");
1056 }
1057 true
1058 } else {
1059 println!("{filename}: FAIL");
1060 false
1061 }
1062}
1063
1064fn read_gzip_header(sig_file: &mut dyn Read) -> Result<GzipHeader> {
1066 let mut head = [0u8; 10];
1067 sig_file.read_exact(&mut head).map_err(Error::Io)?;
1068
1069 if head[0] != 0x1f || head[1] != 0x8b {
1070 return Err(Error::Io(std::io::Error::new(
1071 std::io::ErrorKind::InvalidData,
1072 "Not a gzip file",
1073 )));
1074 }
1075 let flg = head[3];
1076 if (flg & 16) == 0 {
1077 return Err(Error::Io(std::io::Error::new(
1078 std::io::ErrorKind::InvalidData,
1079 "Unsigned gzip archive (no comment)",
1080 )));
1081 }
1082
1083 let mut extra_field = Vec::new();
1085 if (flg & 4) != 0 {
1086 let mut xlen_b = [0u8; 2];
1087 sig_file.read_exact(&mut xlen_b).map_err(Error::Io)?;
1088 extra_field.extend_from_slice(&xlen_b);
1089 let xlen = u64::from(u16::from_le_bytes(xlen_b));
1090 let mut reader = sig_file.take(xlen);
1091 reader.read_to_end(&mut extra_field).map_err(Error::Io)?;
1092 }
1093
1094 let mut name_field = Vec::new();
1095 if (flg & 8) != 0 {
1096 let mut buf = [0u8; 1];
1097 loop {
1098 sig_file.read_exact(&mut buf).map_err(Error::Io)?;
1099 name_field.push(buf[0]);
1100 if buf[0] == 0 {
1101 break;
1102 }
1103 }
1104 }
1105
1106 let mut comment_vec = Vec::new();
1108 let mut buf = [0u8; 1];
1109 loop {
1110 sig_file.read_exact(&mut buf).map_err(Error::Io)?;
1111 if buf[0] == 0 {
1112 break;
1113 }
1114 comment_vec.push(buf[0]);
1115 }
1116
1117 Ok(GzipHeader {
1118 flg,
1119 head,
1120 extra_field,
1121 name_field,
1122 comment_vec,
1123 })
1124}
1125
1126pub fn sign_gzip(
1128 seckey: &[u8],
1129 keynum: [u8; 8],
1130 seckey_path: &Path,
1131 msg_path: &Path,
1132 sig_path: &Path,
1133 comment_bytes: &[u8],
1134) -> Result<()> {
1135 let is_stdout = sig_path.as_os_str() == "-";
1136 let is_stdin = msg_path.as_os_str() == "-";
1137
1138 if is_stdin {
1139 return Err(Error::Io(std::io::Error::new(
1140 std::io::ErrorKind::InvalidInput,
1141 "Gzip signing requires a regular file input (not stdin)",
1142 )));
1143 }
1144
1145 let mut f = OpenOptions::new()
1146 .read(true)
1147 .open(msg_path)
1148 .map_err(Error::Io)?;
1149
1150 let (head, data_start) = parse_gzip_for_signing(&mut f)?;
1151
1152 let (header_msg, _) = hash_gzip_content(&mut f, data_start, seckey_path)?;
1154
1155 let kp = ed25519_compact::KeyPair {
1157 pk: ed25519_compact::PublicKey::from_slice(&seckey[32..]).map_err(Error::Crypto)?,
1158 sk: ed25519_compact::SecretKey::from_slice(seckey).map_err(Error::Crypto)?,
1159 };
1160 let sig = kp
1161 .sk
1162 .sign(&header_msg, Some(ed25519_compact::Noise::default()));
1163
1164 let sig_comment = make_sig_comment(seckey_path, comment_bytes)?;
1165
1166 let sig_header = {
1167 let sig_obj = Sig {
1168 pkalg: PKALG,
1169 keynum,
1170 sig: sig
1171 .as_ref()
1172 .try_into()
1173 .map_err(|_| Error::InvalidSignatureLength)?, };
1175 let encoded = Base64::encode_string(&sig_obj.to_bytes());
1176 let mut h = Vec::new();
1177 h.extend_from_slice(COMMENTHDR.as_bytes());
1178 h.extend_from_slice(&sig_comment);
1179 h.push(b'\n');
1180 h.extend_from_slice(encoded.as_bytes());
1181 h.push(b'\n');
1182 h
1183 };
1184
1185 let mut out: Box<dyn Write> = if is_stdout {
1186 Box::new(stdout())
1187 } else {
1188 Box::new(
1189 OpenOptions::new()
1190 .write(true)
1191 .create_new(true)
1192 .open(sig_path)
1193 .map_err(Error::Io)?,
1194 )
1195 };
1196
1197 write_signed_gzip(
1198 &mut out,
1199 &head,
1200 &sig_header,
1201 &header_msg,
1202 &mut f,
1203 data_start,
1204 )
1205}
1206
1207fn parse_gzip_for_signing(f: &mut std::fs::File) -> Result<([u8; 10], u64)> {
1208 let mut head = [0u8; 10];
1209 f.read_exact(&mut head).map_err(Error::Io)?;
1210
1211 if head[0] != 0x1f || head[1] != 0x8b {
1212 return Err(Error::Io(std::io::Error::new(
1213 std::io::ErrorKind::InvalidData,
1214 "Not a gzip file",
1215 )));
1216 }
1217
1218 let flg = head[3];
1219 if (flg & 4) != 0 {
1220 let mut xlen_b = [0u8; 2];
1221 f.read_exact(&mut xlen_b).map_err(Error::Io)?;
1222 let xlen = i64::from(u16::from_le_bytes(xlen_b));
1223 f.seek(SeekFrom::Current(xlen)).map_err(Error::Io)?;
1224 }
1225
1226 if (flg & 8) != 0 {
1227 let mut buf = [0u8; 1];
1228 loop {
1229 f.read_exact(&mut buf).map_err(Error::Io)?;
1230 if buf[0] == 0 {
1231 break;
1232 }
1233 }
1234 }
1235
1236 if (flg & 16) != 0 {
1237 let mut buf = [0u8; 1];
1238 loop {
1239 f.read_exact(&mut buf).map_err(Error::Io)?;
1240 if buf[0] == 0 {
1241 break;
1242 }
1243 }
1244 }
1245
1246 if (flg & 2) != 0 {
1247 f.seek(SeekFrom::Current(2)).map_err(Error::Io)?;
1248 }
1249
1250 let data_start = f.stream_position().map_err(Error::Io)?;
1251 Ok((head, data_start))
1252}
1253
1254fn hash_gzip_content(
1255 f: &mut std::fs::File,
1256 data_start: u64,
1257 seckey_path: &Path,
1258) -> Result<(Vec<u8>, usize)> {
1259 const BLK_SIZE: usize = 0x0001_0000;
1260 let mut block = vec![0u8; BLK_SIZE];
1261
1262 loop {
1264 match f.read(&mut block) {
1265 Ok(0) => break,
1266 Ok(_) => continue,
1267 Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1268 Err(e) => return Err(Error::Io(e)),
1269 }
1270 }
1271
1272 let mut header_msg = Vec::new();
1273 let time_now = "0000-00-00T00:00:00Z";
1274 let keyname = if seckey_path.as_os_str() == "-" {
1275 "stdin"
1276 } else {
1277 seckey_path.to_str().ok_or(Error::InvalidPath)?
1278 };
1279 write!(
1280 &mut header_msg,
1281 "date={time_now}\nkey={keyname}\nalgorithm=SHA256\nblocksize={BLK_SIZE}\n\n",
1282 )
1283 .map_err(Error::Io)?;
1284
1285 f.seek(SeekFrom::Start(data_start)).map_err(Error::Io)?;
1286 loop {
1287 match f.read(&mut block) {
1288 Ok(0) => break,
1289 Ok(n) => {
1290 let hash = Sha256::digest(&block[..n]);
1291 let hex = HEXLOWER.encode(&hash);
1292 header_msg.extend_from_slice(hex.as_bytes());
1293 header_msg.push(b'\n');
1294 }
1295 Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1296 Err(e) => return Err(Error::Io(e)),
1297 }
1298 }
1299
1300 Ok((header_msg, BLK_SIZE))
1301}
1302
1303fn write_signed_gzip(
1304 out: &mut dyn Write,
1305 head: &[u8; 10],
1306 sig_header: &[u8],
1307 header_msg: &[u8],
1308 input: &mut std::fs::File,
1309 data_start: u64,
1310) -> Result<()> {
1311 let fake_header = [0x1f, 0x8b, 8, 16, 0, 0, 0, 0, head[8], 3];
1312 out.write_all(&fake_header).map_err(Error::Io)?;
1313 out.write_all(sig_header).map_err(Error::Io)?;
1314 out.write_all(header_msg).map_err(Error::Io)?;
1315 out.write_all(&[0u8]).map_err(Error::Io)?;
1316
1317 input.seek(SeekFrom::Start(data_start)).map_err(Error::Io)?;
1318 copy(input, out).map_err(Error::Io)?;
1319 Ok(())
1320}
1321
1322pub fn verify_gzip(
1324 pubkey: Option<&Path>,
1325 msg_path: &Path,
1326 sig_path: &Path,
1327 quiet: bool,
1328) -> Result<()> {
1329 let mut sig_file: Box<dyn Read> = if sig_path.as_os_str() == "-" {
1330 Box::new(stdin())
1331 } else {
1332 Box::new(
1333 OpenOptions::new()
1334 .read(true)
1335 .open(sig_path)
1336 .map_err(Error::Io)?,
1337 )
1338 };
1339
1340 let header = read_gzip_header(sig_file.as_mut())?;
1341
1342 let (sig_obj, header_list) = parse_sig_from_comment(&header.comment_vec)?;
1344
1345 let pub_key_obj = if let Some(path) = pubkey {
1347 let (pk, _) = parse::<PubKey, _>(path, PubKey::from_bytes)?;
1348 pk
1349 } else {
1350 autolocate_key(&header.comment_vec)?
1351 };
1352
1353 if sig_obj.keynum != pub_key_obj.keynum {
1354 return Err(Error::KeyMismatch);
1355 }
1356 crypto::verify(header_list, &pub_key_obj.pubkey, &sig_obj.sig)?;
1357
1358 let header_str = str::from_utf8(header_list).map_err(|_| Error::InvalidCommentHeader)?;
1360 let mut lines = header_str.lines();
1361 let blocksize = parse_header_metadata(&mut lines)?;
1362
1363 let mut out_writer: Option<Box<dyn Write>> = if msg_path.as_os_str() == "-" {
1365 Some(Box::new(stdout()))
1366 } else {
1367 Some(Box::new(
1368 OpenOptions::new()
1369 .write(true)
1370 .create_new(true)
1371 .open(msg_path)
1372 .map_err(Error::Io)?,
1373 ))
1374 };
1375
1376 if let Some(w) = out_writer.as_mut() {
1378 w.write_all(&header.head).map_err(Error::Io)?;
1379 if !header.extra_field.is_empty() {
1380 w.write_all(&header.extra_field).map_err(Error::Io)?;
1381 }
1382 if !header.name_field.is_empty() {
1383 w.write_all(&header.name_field).map_err(Error::Io)?;
1384 }
1385 w.write_all(&header.comment_vec).map_err(Error::Io)?;
1386 w.write_all(&[0u8]).map_err(Error::Io)?;
1387 }
1388
1389 if (header.flg & 2) != 0 {
1390 let mut crc = [0u8; 2];
1391 sig_file.read_exact(&mut crc).map_err(Error::Io)?;
1392 if let Some(w) = out_writer.as_mut() {
1393 w.write_all(&crc).map_err(Error::Io)?;
1394 }
1395 }
1396
1397 verify_payload_blocks(sig_file.as_mut(), lines, blocksize, out_writer.as_mut())?;
1398
1399 if !quiet {
1400 eprintln!("Signature Verified");
1401 }
1402 Ok(())
1403}
1404
1405fn parse_sig_from_comment(comment_vec: &[u8]) -> Result<(Sig, &[u8])> {
1406 let n1 = memchr(b'\n', comment_vec).ok_or(Error::InvalidCommentHeader)?;
1407 let n2_start = n1.checked_add(1).ok_or(Error::Overflow)?;
1408 let n2 = memchr(b'\n', &comment_vec[n2_start..]).ok_or(Error::MissingSignatureNewline)?;
1409 let sig_end = n2_start.checked_add(n2).ok_or(Error::Overflow)?;
1410
1411 let header_list = &comment_vec[sig_end.checked_add(1).ok_or(Error::Overflow)?..];
1412
1413 let b64_bytes = &comment_vec[n2_start..sig_end];
1414 let b64_str = str::from_utf8(b64_bytes).map_err(|_e| Error::InvalidSignatureUtf8)?;
1415 let sig_bytes = Base64::decode_vec(b64_str.trim()).map_err(Error::Base64Decode)?;
1416 let sig_obj = Sig::from_bytes(&sig_bytes)?;
1417
1418 Ok((sig_obj, header_list))
1419}
1420
1421fn parse_header_metadata(lines: &mut std::str::Lines) -> Result<usize> {
1422 let mut algo = "SHA256";
1423 let mut blocksize = 0x0001_0000;
1424
1425 for l in lines.by_ref() {
1426 if l.is_empty() {
1427 break;
1428 }
1429 if let Some(val) = l.strip_prefix("algorithm=") {
1430 algo = val;
1431 } else if let Some(val) = l.strip_prefix("blocksize=") {
1432 blocksize = val.parse().unwrap_or(0x0001_0000);
1433 }
1434 }
1435
1436 if algo != "SHA256" && algo != "SHA512/256" {
1437 return Err(Error::Io(std::io::Error::new(
1438 std::io::ErrorKind::InvalidData,
1439 format!("Unsupported algorithm: {algo}"),
1440 )));
1441 }
1442 Ok(blocksize)
1443}
1444
1445fn verify_payload_blocks(
1446 sig_file: &mut dyn Read,
1447 lines: std::str::Lines,
1448 blocksize: usize,
1449 mut out_writer: Option<&mut Box<dyn Write>>,
1450) -> Result<()> {
1451 let mut buf = vec![0u8; blocksize];
1452 for hash_line in lines {
1453 let n = loop {
1454 match sig_file.read(&mut buf) {
1455 Ok(n) => break n,
1456 Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1457 Err(e) => return Err(Error::Io(e)),
1458 }
1459 };
1460
1461 if n == 0 {
1462 return Err(Error::Io(std::io::Error::new(
1463 std::io::ErrorKind::UnexpectedEof,
1464 "Premature end of archive",
1465 )));
1466 }
1467 let hash = Sha256::digest(&buf[..n]);
1468 let hash_hex = HEXLOWER.encode(&hash);
1469
1470 if hash_hex != hash_line {
1471 return Err(Error::VerifyFailed);
1472 }
1473 if let Some(w) = out_writer.as_mut() {
1474 w.write_all(&buf[..n]).map_err(Error::Io)?;
1475 }
1476 }
1477
1478 let n = loop {
1480 match sig_file.read(&mut buf) {
1481 Ok(n) => break n,
1482 Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1483 Err(e) => return Err(Error::Io(e)),
1484 }
1485 };
1486 if n != 0 {
1487 return Err(Error::Io(std::io::Error::new(
1488 std::io::ErrorKind::InvalidData,
1489 "Trailing data in archive",
1490 )));
1491 }
1492 Ok(())
1493}
1494
1495fn make_sig_comment(seckey_path: &Path, comment_bytes: &[u8]) -> Result<Vec<u8>> {
1496 let mut sig_comment = Vec::new();
1497 if seckey_path.as_os_str() == "-" {
1498 sig_comment.extend_from_slice(b"signature from ");
1499 sig_comment.extend_from_slice(comment_bytes);
1500 } else {
1501 let basename = check_keyname_compliance(None, seckey_path)?;
1502 sig_comment.extend_from_slice(b"verify with ");
1503 sig_comment.extend_from_slice(basename.as_bytes());
1504 sig_comment.extend_from_slice(b".pub");
1505 };
1506 Ok(sig_comment)
1507}