libsignify_rs/
ops.rs

1//
2// signify-rs: cryptographically sign and verify files
3// lib/src/ops.rs: Cryptographic operations
4//
5// Copyright (c) 2025, 2026 Ali Polatel <alip@chesswob.org>
6// Based in part upon OpenBSD's signify which is:
7//   Copyright (c) 2013 Ted Unangst <tedu@openbsd.org>
8//   Copyright (c) 2016 Marc Espie <espie@openbsd.org>
9//   Copyright (c) 2019 Adrian Perez de Castro <aperez@igalia.com>
10//   Copyright (c) 2019 Scott Bennett and other contributors
11//   SPDX-License-Identifier: ISC
12//
13// SPDX-License-Identifier: ISC
14
15use crate::crypto::{
16    self, derive_checksum, generate_keypair, kdf, COMMENTHDR, KDFALG, KEYNUMLEN, PKALG, SALT_LEN,
17    SECKEY_LEN,
18};
19use crate::error::{Error, Result};
20use crate::file::open;
21use crate::file::parse_stream;
22use crate::file::write_stream;
23use crate::file::{parse, EncKey, PubKey, Sig};
24use crate::utils::check_keyname_compliance;
25use crate::utils::check_password_strength;
26use crate::utils::log_untrusted_buf;
27use crate::utils::read_password;
28use base64ct::{Base64, Encoding as _};
29use data_encoding::HEXLOWER;
30use memchr::memchr;
31use memchr::memmem;
32use rand_core::{OsRng, TryRngCore as _};
33use sha2::{Digest as _, Sha256, Sha512};
34use std::fs::File;
35use std::io::stdout;
36
37use std::io::Cursor;
38use std::io::{copy, stderr, stdin};
39use std::io::{Read, Seek, SeekFrom, Write};
40use std::path::{Path, PathBuf};
41use std::str;
42use zeroize::Zeroizing;
43
44type EmbeddedSigResult = Result<(Sig, Vec<u8>, Box<dyn Read>)>;
45
46/// Gzip header structure.
47struct GzipHeader {
48    /// Gzip flags.
49    flg: u8,
50    /// Header bytes (magic, compression method, flags, mtime, xfl, os).
51    head: [u8; 10],
52    /// Extra field data.
53    extra_field: Vec<u8>,
54    /// Original filename field.
55    name_field: Vec<u8>,
56    /// Comment field.
57    comment_vec: Vec<u8>,
58}
59
60/// Builder for key generation operations.
61///
62/// # Examples
63///
64/// ```rust
65/// use libsignify_rs::KeyGenerator;
66/// use tempfile::tempdir;
67///
68/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
69/// let dir = tempdir()?;
70/// let pub_path = dir.path().join("key.pub");
71/// let sec_path = dir.path().join("key.sec");
72///
73/// // Generate with default settings (secure KDF rounds)
74/// KeyGenerator::new()
75///     .rounds(0) // No encryption for automated testing
76///     .comment("test-key")
77///     .generate(&pub_path, &sec_path)?;
78///
79/// assert!(pub_path.exists());
80/// assert!(sec_path.exists());
81/// # Ok(())
82/// # }
83/// ```
84#[derive(Default)]
85pub struct KeyGenerator {
86    rounds: u32,
87    comment: Option<String>,
88    key_id: Option<i32>,
89    tty_handle: Option<File>,
90}
91
92impl KeyGenerator {
93    /// Create a new `KeyGenerator` with default settings.
94    #[must_use]
95    pub fn new() -> Self {
96        Self {
97            rounds: crypto::DEFAULT_ROUNDS,
98            comment: None,
99            key_id: None,
100            tty_handle: None,
101        }
102    }
103
104    /// Set TTY handle for interactive password prompts.
105    #[must_use]
106    pub fn tty_handle(mut self, tty: File) -> Self {
107        self.tty_handle = Some(tty);
108        self
109    }
110
111    /// Set KDF rounds (0 for no encryption).
112    ///
113    /// Default is 42.
114    #[must_use]
115    pub fn rounds(mut self, rounds: u32) -> Self {
116        self.rounds = rounds;
117        self
118    }
119
120    /// Set key comment.
121    ///
122    /// Default is "signify key".
123    #[must_use]
124    pub fn comment(mut self, comment: impl Into<String>) -> Self {
125        self.comment = Some(comment.into());
126        self
127    }
128
129    /// Set key ID for keyring integration.
130    #[must_use]
131    pub fn key_id(mut self, key_id: i32) -> Self {
132        self.key_id = Some(key_id);
133        self
134    }
135
136    /// Execute key generation writing to streams.
137    ///
138    /// # Errors
139    /// Returns errors if I/O or KDF fails.
140    pub fn generate_io<W1: Write, W2: Write>(
141        self,
142        mut pub_writer: W1,
143        mut sec_writer: W2,
144    ) -> Result<()> {
145        let comment = self.comment.as_deref().unwrap_or("signify");
146
147        let mut keynum = [0_u8; KEYNUMLEN];
148        OsRng.try_fill_bytes(&mut keynum)?;
149
150        let (public_key, secret_key) = generate_keypair()?;
151
152        let (enc_key, pub_key_struct) = if self.rounds > 0 {
153            let pass = prompt_password(true, self.key_id, self.tty_handle.as_ref())?;
154            let mut salt = [0u8; SALT_LEN];
155            OsRng.try_fill_bytes(&mut salt)?;
156
157            let mut xorkey = Zeroizing::new(vec![0u8; SECKEY_LEN]);
158            kdf(&pass, &salt, self.rounds, &mut xorkey)?;
159
160            let mut seckey = Zeroizing::new([0u8; SECKEY_LEN]);
161            for i in 0..SECKEY_LEN {
162                seckey[i] = secret_key[i] ^ xorkey[i];
163            }
164
165            let checksum = derive_checksum(secret_key.as_ref());
166            let enc_key = EncKey {
167                pkalg: PKALG,
168                kdfalg: KDFALG,
169                kdfrounds: self.rounds,
170                salt,
171                checksum,
172                keynum,
173                seckey,
174            };
175
176            let pk = PubKey {
177                pkalg: PKALG,
178                keynum,
179                pubkey: public_key,
180            };
181
182            (enc_key, pk)
183        } else {
184            let enc_key = EncKey {
185                pkalg: PKALG,
186                kdfalg: KDFALG,
187                kdfrounds: 0,
188                salt: [0u8; SALT_LEN],
189                checksum: derive_checksum(secret_key.as_ref()),
190                keynum,
191                seckey: Zeroizing::new(*secret_key),
192            };
193
194            let pk = PubKey {
195                pkalg: PKALG,
196                keynum,
197                pubkey: public_key,
198            };
199            (enc_key, pk)
200        };
201
202        // Write secret key.
203        let comment_len = comment
204            .len()
205            .checked_add(" secret key".len())
206            .ok_or(Error::Overflow)?;
207        let mut seckey_comment = Vec::with_capacity(comment_len);
208        seckey_comment.extend_from_slice(comment.as_bytes());
209        seckey_comment.extend_from_slice(b" secret key");
210        write_stream(&mut sec_writer, &seckey_comment, &enc_key.to_bytes())?;
211
212        let mut pubkey_comment = Vec::with_capacity(comment_len);
213        pubkey_comment.extend_from_slice(comment.as_bytes());
214        pubkey_comment.extend_from_slice(b" public key");
215        write_stream(&mut pub_writer, &pubkey_comment, &pub_key_struct.to_bytes())
216    }
217
218    /// Execute key generation.
219    ///
220    /// # Errors
221    /// Returns errors if file I/O or KDF fails.
222    pub fn generate(self, pubkey_path: &Path, seckey_path: &Path) -> Result<()> {
223        let pub_file = open(pubkey_path, true)?;
224        let sec_file = open(seckey_path, true)?;
225        self.generate_io(pub_file, sec_file)
226    }
227}
228
229/// Builder for signing operations.
230///
231/// # Examples
232///
233/// Basic signing:
234///
235/// ```rust
236/// use libsignify_rs::{KeyGenerator, Signer};
237/// use tempfile::tempdir;
238/// use std::fs;
239///
240/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
241/// let dir = tempdir()?;
242/// let pub_path = dir.path().join("key.pub");
243/// let sec_path = dir.path().join("key.sec");
244/// let msg_path = dir.path().join("msg.txt");
245/// let sig_path = dir.path().join("msg.sig");
246///
247/// // Setup: Generate keys and message
248/// KeyGenerator::new().rounds(0).generate(&pub_path, &sec_path)?;
249/// fs::write(&msg_path, "test message")?;
250///
251/// // Sign
252/// Signer::new()
253///     .seckey(&sec_path)
254///     .sign(&msg_path, &sig_path)?;
255///
256/// assert!(sig_path.exists());
257/// # Ok(())
258/// # }
259/// ```
260///
261/// Embedded signature:
262///
263/// ```rust
264/// # use libsignify_rs::{KeyGenerator, Signer};
265/// # use tempfile::tempdir;
266/// # use std::fs;
267/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
268/// # let dir = tempdir()?;
269/// # let pub_path = dir.path().join("key.pub");
270/// # let sec_path = dir.path().join("key.sec");
271/// # let msg_path = dir.path().join("msg.txt");
272/// # let sig_path = dir.path().join("msg.sig");
273/// # KeyGenerator::new().rounds(0).generate(&pub_path, &sec_path)?;
274/// # fs::write(&msg_path, "test message")?;
275/// Signer::new()
276///     .seckey(&sec_path)
277///     .embed(true)
278///     .sign(&msg_path, &sig_path)?;
279/// # Ok(())
280/// # }
281/// ```
282///
283/// Gzip signing:
284///
285/// ```rust
286/// # use libsignify_rs::{KeyGenerator, Signer};
287/// # use tempfile::tempdir;
288/// # use std::fs;
289/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
290/// # let dir = tempdir()?;
291/// # let pub_path = dir.path().join("key.pub");
292/// # let sec_path = dir.path().join("key.sec");
293/// # let msg_path = dir.path().join("archive.gz"); // Input must be a file
294/// # let sig_path = dir.path().join("archive.gz.sig"); // Output is signed gzip
295/// # KeyGenerator::new().rounds(0).generate(&pub_path, &sec_path)?;
296/// use flate2::write::GzEncoder;
297/// use flate2::Compression;
298/// use std::fs::File;
299/// use std::io::Write;
300///
301/// // Create a valid gzip file to sign
302/// let f = File::create(&msg_path)?;
303/// let mut e = GzEncoder::new(f, Compression::default());
304/// e.write_all(b"compressed content")?;
305/// e.finish()?;
306///
307/// Signer::new()
308///     .seckey(&sec_path)
309///     .gzip(true)
310///     .sign(&msg_path, &sig_path)?;
311/// # Ok(())
312/// # }
313/// ```
314pub struct Signer {
315    seckey: Option<PathBuf>,
316    embed: bool,
317    gzip: bool,
318    key_id: Option<i32>,
319    tty_handle: Option<File>,
320}
321
322impl Default for Signer {
323    fn default() -> Self {
324        Self::new()
325    }
326}
327
328impl Signer {
329    /// Create a new `Signer`.
330    #[must_use]
331    pub fn new() -> Self {
332        Self {
333            seckey: None,
334            embed: false,
335            gzip: false,
336            key_id: None,
337            tty_handle: None,
338        }
339    }
340
341    /// Set TTY handle for interactive password prompts.
342    #[must_use]
343    pub fn tty_handle(mut self, tty: File) -> Self {
344        self.tty_handle = Some(tty);
345        self
346    }
347
348    /// Set secret key path.
349    #[must_use]
350    pub fn seckey(mut self, path: impl Into<PathBuf>) -> Self {
351        self.seckey = Some(path.into());
352        self
353    }
354
355    /// Set embed mode (signify -e).
356    ///
357    /// If true, creates an embedded signature.
358    #[must_use]
359    pub fn embed(mut self, embed: bool) -> Self {
360        self.embed = embed;
361        self
362    }
363
364    /// Set gzip mode (signify -z).
365    ///
366    /// If true, signs a gzip archive.
367    #[must_use]
368    pub fn gzip(mut self, gzip: bool) -> Self {
369        self.gzip = gzip;
370        self
371    }
372
373    /// Set key ID (for keyring support).
374    #[must_use]
375    pub fn key_id(mut self, key_id: i32) -> Self {
376        self.key_id = Some(key_id);
377        self
378    }
379
380    /// Sign a file.
381    ///
382    /// # Errors
383    /// Returns errors if I/O, decryption, or signing fails.
384    pub fn sign(self, msg_path: &Path, sig_path: &Path) -> Result<()> {
385        self.sign_io(msg_path, sig_path, None, None, None)
386    }
387
388    /// Sign a message using optional file handles.
389    pub fn sign_io(
390        &self,
391        msg_path: &Path,
392        sig_path: &Path,
393        msg_file: Option<File>,
394        sig_file: Option<File>,
395        seckey_file: Option<File>,
396    ) -> Result<()> {
397        let seckey_path = self.seckey.as_deref().ok_or(Error::RequiredArg("-s"))?;
398
399        let (enc_key, comment_bytes) = if let Some(f) = seckey_file {
400            parse_stream(f, EncKey::from_bytes)?
401        } else {
402            parse::<EncKey, _>(seckey_path, EncKey::from_bytes)?
403        };
404
405        let xorkey = if enc_key.kdfrounds > 0 {
406            let pass = Zeroizing::new(prompt_password(
407                false,
408                self.key_id,
409                self.tty_handle.as_ref(),
410            )?);
411            let mut xorkey = Zeroizing::new(vec![0u8; SECKEY_LEN]);
412            kdf(&pass, &enc_key.salt, enc_key.kdfrounds, &mut xorkey)?;
413            Some(xorkey)
414        } else {
415            None
416        };
417
418        let mut seckey = Zeroizing::new([0u8; SECKEY_LEN]); // 64
419        if let Some(x) = xorkey {
420            for i in 0..SECKEY_LEN {
421                seckey[i] = enc_key.seckey[i] ^ x[i];
422            }
423        } else {
424            seckey.copy_from_slice(enc_key.seckey.as_ref());
425        }
426
427        // Verify checksum to ensure the password was correct.
428        let checksum = derive_checksum(seckey.as_ref());
429        if checksum != enc_key.checksum {
430            return Err(Error::IncorrectPassphrase);
431        }
432
433        // Determine paths and streams.
434        let is_stdout = sig_path.to_str() == Some("-");
435        let is_stdin = msg_path.to_str() == Some("-");
436
437        if self.gzip {
438            if self.embed {
439                return Err(Error::Io(std::io::Error::new(
440                    std::io::ErrorKind::InvalidInput,
441                    "cannot combine -e (embed) and -z (gzip)",
442                )));
443            }
444            return sign_gzip(
445                seckey.as_ref(),
446                enc_key.keynum,
447                seckey_path,
448                msg_path,
449                sig_path,
450                &comment_bytes,
451                msg_file,
452                sig_file,
453            );
454        }
455
456        // Pre-calculate signature comment (used in headers).
457        let mut sig_comment = Vec::new();
458        if seckey_path.to_str() == Some("-") {
459            sig_comment.extend_from_slice(b"signature from ");
460            sig_comment.extend_from_slice(&comment_bytes);
461        } else {
462            sig_comment.extend_from_slice(b"verify with ");
463            sig_comment.extend_from_slice(
464                seckey_path
465                    .file_stem()
466                    .unwrap_or(seckey_path.as_os_str())
467                    .as_encoded_bytes(),
468            );
469            sig_comment.extend_from_slice(b".pub");
470        };
471
472        sign_standard(SignParams {
473            seckey: &seckey,
474            keynum: enc_key.keynum,
475            msg_path,
476            sig_path,
477            embed: self.embed,
478            is_stdout,
479            is_stdin,
480            sig_comment: &sig_comment,
481            msg_file,
482            sig_file,
483        })
484    }
485}
486
487/// Builder for verification operations.
488///
489/// # Examples
490///
491/// Basic verification:
492///
493/// ```rust
494/// use libsignify_rs::{KeyGenerator, Signer, Verifier};
495/// use tempfile::tempdir;
496/// use std::fs;
497///
498/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
499/// let dir = tempdir()?;
500/// let pub_path = dir.path().join("key.pub");
501/// let sec_path = dir.path().join("key.sec");
502/// let msg_path = dir.path().join("msg.txt");
503/// let sig_path = dir.path().join("msg.sig");
504///
505/// // Setup
506/// KeyGenerator::new().rounds(0).generate(&pub_path, &sec_path)?;
507/// fs::write(&msg_path, "test message")?;
508/// Signer::new().seckey(&sec_path).sign(&msg_path, &sig_path)?;
509///
510/// // Verify
511/// Verifier::new()
512///     .pubkey(&pub_path)
513///     .quiet(true)
514///     .verify(&msg_path, &sig_path)?;
515/// # Ok(())
516/// # }
517/// ```
518///
519/// Embedded verification:
520///
521/// ```rust
522/// # use libsignify_rs::{KeyGenerator, Signer, Verifier};
523/// # use tempfile::tempdir;
524/// # use std::fs;
525/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
526/// # let dir = tempdir()?;
527/// # let pub_path = dir.path().join("key.pub");
528/// # let sec_path = dir.path().join("key.sec");
529/// # let msg_path = dir.path().join("msg.txt");
530/// # let sig_path = dir.path().join("msg.sig");
531/// # KeyGenerator::new().rounds(0).generate(&pub_path, &sec_path)?;
532/// # fs::write(&msg_path, "test message")?;
533/// // Sign with embed
534/// Signer::new()
535///     .seckey(&sec_path)
536///     .embed(true)
537///     .sign(&msg_path, &sig_path)?;
538///
539/// // Remove original message to verify extraction
540/// fs::remove_file(&msg_path)?;
541///
542/// // Verify embedded
543/// Verifier::new()
544///     .pubkey(&pub_path)
545///     .quiet(true)
546///     .embed(true)
547///     .verify(&msg_path, &sig_path)?;
548/// # Ok(())
549/// # }
550/// ```
551///
552/// Gzip verification:
553///
554/// ```rust
555/// # use libsignify_rs::{KeyGenerator, Signer, Verifier};
556/// # use tempfile::tempdir;
557/// # use std::fs::File;
558/// # use std::path::Path;
559/// # use flate2::{write::GzEncoder, Compression};
560/// # use std::io::Write;
561/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
562/// # let dir = tempdir()?;
563/// # let pub_path = dir.path().join("key.pub");
564/// # let sec_path = dir.path().join("key.sec");
565/// # let msg_path = dir.path().join("archive.gz");
566/// # let sig_path = dir.path().join("archive.gz.sig");
567/// # KeyGenerator::new().rounds(0).generate(&pub_path, &sec_path)?;
568/// # let f = File::create(&msg_path)?;
569/// # let mut e = GzEncoder::new(f, Compression::default());
570/// # e.write_all(b"data")?;
571/// # e.finish()?;
572/// // Sign gzip
573/// Signer::new()
574///     .seckey(&sec_path)
575///     .gzip(true)
576///     .sign(&msg_path, &sig_path)?;
577///
578/// // Verify gzip
579/// Verifier::new()
580///     .pubkey(&pub_path)
581///     .quiet(true)
582///     .gzip(true)
583///     .verify(&Path::new("-"), &sig_path)?; // Output to stdout (-) or file
584/// # Ok(())
585/// # }
586/// ```
587///
588/// Handling verification failures:
589///
590/// ```rust
591/// # use libsignify_rs::{KeyGenerator, Signer, Verifier};
592/// # use tempfile::tempdir;
593/// # use std::fs;
594/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
595/// # let dir = tempdir()?;
596/// # let pub_path = dir.path().join("key.pub");
597/// # let sec_path = dir.path().join("key.sec");
598/// # let msg_path = dir.path().join("msg.txt");
599/// # let sig_path = dir.path().join("msg.sig");
600/// # KeyGenerator::new().rounds(0).generate(&pub_path, &sec_path)?;
601/// # fs::write(&msg_path, "original message")?;
602/// # Signer::new().seckey(&sec_path).sign(&msg_path, &sig_path)?;
603///
604/// // Tamper with the message
605/// fs::write(&msg_path, "tampered message")?;
606///
607/// // Verification should fail
608/// let result = Verifier::new()
609///     .pubkey(&pub_path)
610///     .verify(&msg_path, &sig_path);
611///
612/// assert!(result.is_err());
613/// # Ok(())
614/// # }
615/// ```
616pub struct Verifier {
617    pubkey: Option<PathBuf>,
618    quiet: bool,
619    embed: bool,
620    gzip: bool,
621}
622
623impl Default for Verifier {
624    fn default() -> Self {
625        Self::new()
626    }
627}
628
629impl Verifier {
630    /// Create a new `Verifier`.
631    #[must_use]
632    pub fn new() -> Self {
633        Self {
634            pubkey: None,
635            quiet: false,
636            embed: false,
637            gzip: false,
638        }
639    }
640
641    /// Set public key path.
642    #[must_use]
643    pub fn pubkey(mut self, path: impl Into<PathBuf>) -> Self {
644        self.pubkey = Some(path.into());
645        self
646    }
647
648    /// Set quiet mode.
649    ///
650    /// If true, suppresses "Signature Verified" output.
651    #[must_use]
652    pub fn quiet(mut self, quiet: bool) -> Self {
653        self.quiet = quiet;
654        self
655    }
656
657    /// Set embed mode (signify -I).
658    ///
659    /// Used when verifying an embedded signature.
660    #[must_use]
661    pub fn embed(mut self, embed: bool) -> Self {
662        self.embed = embed;
663        self
664    }
665
666    /// Set gzip mode (signify -z).
667    ///
668    /// Used when verifying a signed gzip archive.
669    #[must_use]
670    pub fn gzip(mut self, gzip: bool) -> Self {
671        self.gzip = gzip;
672        self
673    }
674
675    /// Verify a signature.
676    ///
677    /// # Errors
678    /// Returns errors if verification fails.
679    pub fn verify(self, msg_path: &Path, sig_path: &Path) -> Result<()> {
680        self.verify_io(msg_path, sig_path, None, None, None)
681    }
682
683    /// Verify with pre-opened resources.
684    pub fn verify_io(
685        self,
686        msg_path: &Path,
687        sig_path: &Path,
688        mut msg_file: Option<File>,
689        mut sig_file: Option<File>,
690        pubkey_file: Option<File>,
691    ) -> Result<()> {
692        if self.gzip {
693            return verify_gzip(
694                self.pubkey.as_deref(),
695                msg_path,
696                sig_path,
697                self.quiet,
698                msg_file,
699                sig_file,
700                pubkey_file,
701            );
702        }
703
704        // Standard embedded/detached verify:
705        // Determine signature and input stream.
706        let (sig, stream, output_path) = if self.embed {
707            // Embed mode: msg_path is output (extracted msg).
708            // Sig is input. extract signature from sig_path/sig_file.
709            let (sig, prelude, rest_reader) = if let Some(f) = sig_file.take() {
710                parse_embedded_signature_reader(Box::new(f))?
711            } else {
712                parse_embedded_signature(sig_path)?
713            };
714            let stream = Cursor::new(prelude).chain(rest_reader);
715
716            let out_path = if self.embed { Some(msg_path) } else { None };
717            (sig, Box::new(stream) as Box<dyn Read>, out_path)
718        } else {
719            // Detached
720            let (sig, _comment) = if let Some(f) = sig_file.take() {
721                let (sig, comment) = parse_stream(f, Sig::from_bytes)?;
722                (sig, comment)
723            } else {
724                parse::<Sig, _>(sig_path, Sig::from_bytes)?
725            };
726
727            let file: Box<dyn Read> = if let Some(f) = msg_file.take() {
728                Box::new(f)
729            } else {
730                Box::new(open(msg_path, false)?)
731            };
732
733            // Detached verification never extracts/writes.
734            (sig, file, None)
735        };
736
737        let pubkey = if let Some(f) = pubkey_file {
738            let (pk, _) = parse_stream(f, PubKey::from_bytes)?;
739            pk
740        } else if let Some(path) = &self.pubkey {
741            let (pk, _) = parse::<PubKey, _>(path, PubKey::from_bytes)?;
742            pk
743        } else {
744            return Err(Error::MissingPubKey);
745        };
746
747        if sig.keynum != pubkey.keynum {
748            return Err(Error::KeyMismatch);
749        }
750
751        // Prepare output writer if needed.
752        let mut writer: Option<Box<dyn Write>> = if let Some(f) = msg_file.take() {
753            // msg_file provided. Used for output if embed?
754            Some(Box::new(f))
755        } else if let Some(path) = output_path {
756            // Standard path open.
757            if path.to_str() == Some("-") {
758                Some(Box::new(stdout()))
759            } else {
760                Some(Box::new(open(path, true)?))
761            }
762        } else {
763            None
764        };
765
766        // Verify stream.
767        let writer_ref = writer.as_mut().map(|w| &mut **w as &mut dyn Write);
768        crypto::verify_stream(stream, writer_ref, &pubkey.pubkey, &sig.sig)?;
769
770        if !self.quiet {
771            println!("Signature Verified");
772        }
773
774        Ok(())
775    }
776}
777
778fn parse_embedded_signature_reader(mut reader: Box<dyn Read>) -> EmbeddedSigResult {
779    // Read header bounded.
780    const HEADER_LIMIT: usize = 4096;
781    let mut buffer = vec![0_u8; HEADER_LIMIT];
782    let mut valid_len = 0;
783
784    // Read up to limit.
785    while valid_len < HEADER_LIMIT {
786        let n = reader.read(&mut buffer[valid_len..]).map_err(Error::Io)?;
787        if n == 0 {
788            break;
789        }
790        valid_len = valid_len.checked_add(n).ok_or(Error::Overflow)?;
791    }
792    buffer.truncate(valid_len);
793
794    let n1 = memchr(b'\n', &buffer).ok_or(Error::InvalidCommentHeader)?;
795    let n2_start = n1.checked_add(1).ok_or(Error::Overflow)?;
796    let n2 = memchr(b'\n', &buffer[n2_start..]).ok_or(Error::MissingSignatureNewline)?;
797    let b64_start = n2_start;
798    let b64_end = b64_start.checked_add(n2).ok_or(Error::Overflow)?;
799
800    if b64_end > buffer.len() {
801        return Err(Error::InvalidCommentHeader);
802    }
803
804    let b64_bytes = &buffer[b64_start..b64_end];
805    let b64_str = str::from_utf8(b64_bytes).map_err(|_e| Error::InvalidSignatureUtf8)?;
806    let sig_bytes = Base64::decode_vec(b64_str.trim()).map_err(Error::Base64Decode)?;
807    let sig = Sig::from_bytes(&sig_bytes)?;
808
809    let msg_start = b64_end.checked_add(1).ok_or(Error::Overflow)?;
810
811    // Construct prelude: (buffer[msg_start..]).
812    let prelude = if msg_start < buffer.len() {
813        buffer[msg_start..].to_vec()
814    } else {
815        Vec::new()
816    };
817
818    Ok((sig, prelude, reader))
819}
820
821/// Prompt for password.
822fn prompt_password(
823    confirm: bool,
824    key_id: Option<i32>,
825    tty: Option<&File>,
826) -> Result<Zeroizing<Vec<u8>>> {
827    let pass = loop {
828        let pass = read_password("passphrase: ", key_id, tty)?;
829
830        // Check password strength only during key generation (confirm=true).
831        // During signing, the password decrypts an existing key so strength doesn't matter.
832        if !confirm {
833            return Ok(pass);
834        }
835
836        match check_password_strength(&pass) {
837            Ok(()) if key_id.is_some() => return Ok(pass),
838            Err(error) if key_id.is_some() => return Err(error),
839            Err(error) => {
840                let mut msg = "signify: ".to_string();
841                msg.push_str(&error.to_string());
842                msg.push('\n');
843                stderr().write_all(msg.as_bytes()).map_err(Error::Io)?;
844                stderr().flush().map_err(Error::Io)?;
845                continue;
846            }
847            _ => break pass,
848        }
849    };
850
851    for _ in 0..3 {
852        stderr().flush().map_err(Error::Io)?;
853        let pass2 = read_password("confirm passphrase: ", None, tty)?;
854        if pass == pass2 {
855            return Ok(pass);
856        }
857        let mut msg = "signify: ".to_string();
858        msg.push_str(&Error::PasswordMismatch.to_string());
859        msg.push('\n');
860        stderr().write_all(msg.as_bytes()).map_err(Error::Io)?;
861        stderr().flush().map_err(Error::Io)?;
862    }
863
864    Err(Error::PasswordMismatch)
865}
866
867/// Parameters for signing operations.
868struct SignParams<'a> {
869    /// Secret key bytes.
870    seckey: &'a [u8; 64],
871    /// Key ID.
872    keynum: [u8; 8],
873    /// Path to the message file.
874    msg_path: &'a Path,
875    /// Path to the signature file.
876    sig_path: &'a Path,
877    /// Whether to embed the signature.
878    embed: bool,
879    /// Whether output is stdout.
880    is_stdout: bool,
881    /// Whether input is stdin.
882    is_stdin: bool,
883    /// Signature comment to include.
884    sig_comment: &'a [u8],
885    /// Pre-opened message file.
886    msg_file: Option<File>,
887    /// Pre-opened signature file.
888    sig_file: Option<File>,
889}
890
891fn sign_standard(params: SignParams) -> Result<()> {
892    // Helper to create header bytes from signature bytes.
893    let make_header = |sig_bytes: &[u8]| -> Result<Vec<u8>> {
894        // Unwrap is safe because it's fixed length.
895        #[expect(clippy::disallowed_methods)]
896        let sig = Sig {
897            pkalg: PKALG,
898            keynum: params.keynum,
899            sig: sig_bytes.try_into().unwrap(),
900        };
901        let encoded = Base64::encode_string(&sig.to_bytes());
902        let mut h = Vec::new();
903        h.extend_from_slice(COMMENTHDR.as_bytes());
904        h.extend_from_slice(params.sig_comment);
905        h.push(b'\n');
906        h.extend_from_slice(encoded.as_bytes());
907        h.push(b'\n');
908        Ok(h)
909    };
910
911    let out_file = if let Some(f) = params.sig_file {
912        Some(f)
913    } else if params.is_stdout {
914        None
915    } else {
916        Some(open(params.sig_path, true)?)
917    };
918
919    if params.embed {
920        if let Some(mut file) = out_file {
921            // File output: Seek and patch.
922            // 1. Write dummy header.
923            let dummy_sig = [0u8; 64];
924            let header = make_header(&dummy_sig)?;
925            file.write_all(&header).map_err(Error::Io)?;
926
927            // 2. Stream input.
928            let mut reader: Box<dyn Read> = if let Some(f) = params.msg_file {
929                Box::new(f)
930            } else if params.is_stdin {
931                Box::new(stdin())
932            } else {
933                Box::new(open(params.msg_path, false)?)
934            };
935
936            let mut buf = Vec::new();
937            reader.read_to_end(&mut buf).map_err(Error::Io)?;
938            file.write_all(&buf).map_err(Error::Io)?;
939
940            // 3. Patch.
941            let real_sig = crypto::sign(&buf, params.seckey)?;
942            let real_header = make_header(real_sig.as_ref())?;
943            if real_header.len() != header.len() {
944                return Err(Error::InvalidSignatureLength);
945            }
946            file.rewind().map_err(Error::Io)?;
947            file.write_all(&real_header).map_err(Error::Io)?;
948        } else {
949            // Output is stdout.
950            if params.is_stdin {
951                let mut buf = Vec::new();
952                stdin().read_to_end(&mut buf).map_err(Error::Io)?;
953
954                let sig = crypto::sign(&buf, params.seckey)?;
955                let header = make_header(sig.as_ref())?;
956
957                let mut out = stdout();
958                out.write_all(&header).map_err(Error::Io)?;
959                out.write_all(&buf).map_err(Error::Io)?;
960            } else {
961                // File input: Two pass (read hash, read write).
962                let mut file = if let Some(file) = params.msg_file {
963                    file
964                } else {
965                    open(params.msg_path, false)?
966                };
967
968                // Ensure start
969                // If it's a file, seek to start.
970                let _ = file.rewind();
971
972                let sig = crypto::sign_stream(&mut file, params.seckey)?;
973                let header = make_header(&sig)?;
974                let mut out = stdout();
975                out.write_all(&header).map_err(Error::Io)?;
976
977                // Rewind.
978                file.rewind().map_err(Error::Io)?;
979                copy(&mut file, &mut out).map_err(Error::Io)?;
980            }
981        }
982    } else {
983        // Detached (-S)
984        let mut reader: Box<dyn Read> = if let Some(f) = params.msg_file {
985            Box::new(f)
986        } else if params.is_stdin {
987            Box::new(stdin())
988        } else {
989            Box::new(open(params.msg_path, false)?)
990        };
991        let sig = crypto::sign_stream(&mut reader, params.seckey)?;
992        let header = make_header(&sig)?;
993
994        if let Some(mut f) = out_file {
995            f.write_all(&header).map_err(Error::Io)?;
996        } else {
997            stdout().write_all(&header).map_err(Error::Io)?;
998        }
999    }
1000
1001    Ok(())
1002}
1003
1004/// Parse embedded signature, returning sig, prelude bytes, and rest reader.
1005///
1006/// # Errors
1007/// Returns errors from file I/O or parsing.
1008fn 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
1017/// Check a list of checksums.
1018///
1019/// # Errors
1020///
1021/// Returns `Error::Io` on file I/O errors.
1022/// Returns `Error::KeyMismatch` if keynum mismatch.
1023/// Returns `Error::VerifyFailed` if signature verification fails.
1024/// Returns `Error::CheckFailed` if one or more checksums fail verification.
1025pub fn check_checksums(
1026    pubkey_path: Option<&Path>,
1027    sig_path: &Path,
1028    mut sig_file: Option<File>,
1029    quiet: bool,
1030) -> Result<()> {
1031    let (sig, mut prelude, mut reader) = if let Some(f) = sig_file.take() {
1032        parse_embedded_signature_reader(Box::new(f))?
1033    } else {
1034        parse_embedded_signature(sig_path)?
1035    };
1036    reader.read_to_end(&mut prelude).map_err(Error::Io)?;
1037    let msg = prelude;
1038
1039    // Public key is required.
1040    let path = pubkey_path.ok_or(Error::MissingPubKey)?;
1041    let (pubkey, _) = parse::<PubKey, _>(path, PubKey::from_bytes)?;
1042
1043    if sig.keynum != pubkey.keynum {
1044        return Err(Error::KeyMismatch);
1045    }
1046
1047    crypto::verify(&msg, &pubkey.pubkey, &sig.sig)?;
1048
1049    let mut failed = false;
1050    for (line_num, line) in msg.split(|&b| matches!(b, b'\n' | b'\r')).enumerate() {
1051        let line = line.trim_ascii();
1052        if line.is_empty() {
1053            continue;
1054        }
1055
1056        if let Err(error) = verify_checksum_line(line, quiet) {
1057            eprintln!("signify: line {}: {error}", line_num.saturating_add(1));
1058            failed = true;
1059        }
1060    }
1061
1062    if failed {
1063        Err(Error::CheckFailed)
1064    } else {
1065        Ok(())
1066    }
1067}
1068
1069/// Verify a single line from a checksum file.
1070///
1071/// Supports BSD format (`ALGO (FILENAME) = HASH`), and
1072/// GNU coreutils format (`HASH  FILENAME` or `HASH *FILENAME`).
1073///
1074/// # Errors
1075///
1076/// Returns `Error::ChecksumParseFailed` if the line cannot be parsed.
1077/// Returns `Error::ChecksumMismatch` if the hash does not match.
1078/// Returns `Error::UnsupportedHashAlgo` for unknown algorithms.
1079/// Returns I/O errors if the file cannot be read.
1080pub fn verify_checksum_line(line: &[u8], quiet: bool) -> Result<()> {
1081    let (algo, hash, path) = parse_checksum_line(line)
1082        .ok_or_else(|| Error::ChecksumParseFailed(log_untrusted_buf(line)))?;
1083    verify_file_with_hash(algo, hash, &path, quiet)
1084}
1085
1086/// Parse a checksum line in any supported format.
1087///
1088/// Returns (algo, hash, filename) on success.
1089fn parse_checksum_line(line: &[u8]) -> Option<(&[u8], &[u8], &str)> {
1090    try_parse_bsd_format(line).or_else(|| try_parse_gnu_format(line))
1091}
1092
1093/// Try to parse BSD format: `ALGO (FILENAME) = HASH`
1094fn try_parse_bsd_format(line: &[u8]) -> Option<(&[u8], &[u8], &str)> {
1095    let marker = b" = ";
1096    let idx = memmem::find(line, marker)?;
1097
1098    let left = &line[..idx];
1099    let right_start = idx.checked_add(marker.len())?;
1100    let right = &line[right_start..];
1101    let hash = right.trim_ascii();
1102
1103    // Parse left: "ALGO (FILENAME)" -> Find first space
1104    let space_idx = memchr(b' ', left)?;
1105    let algo = &left[..space_idx];
1106    let rest_start = space_idx.checked_add(1)?;
1107    let rest = &left[rest_start..];
1108
1109    // Rest should be "(FILENAME)"
1110    if rest.len() < 2 || rest.first() != Some(&b'(') || rest.last() != Some(&b')') {
1111        return None;
1112    }
1113
1114    let filename = &rest[1..rest.len().checked_sub(1)?];
1115    let filename = std::str::from_utf8(filename).ok()?;
1116    if !filename.is_empty() {
1117        Some((algo, hash, filename))
1118    } else {
1119        None
1120    }
1121}
1122
1123/// Try to parse GNU coreutils format: `HASH  FILENAME` or `HASH *FILENAME`.
1124///
1125/// Returns (algo, hash, filename) on success, None if format doesn't match.
1126fn try_parse_gnu_format(line: &[u8]) -> Option<(&[u8], &[u8], &str)> {
1127    // Find separator: Two spaces or space+asterisk.
1128    let (sep_idx, sep_len) = if let Some(idx) = memmem::find(line, b"  ") {
1129        (idx, 2)
1130    } else if let Some(idx) = memmem::find(line, b" *") {
1131        (idx, 2)
1132    } else {
1133        return None;
1134    };
1135
1136    let hash = &line[..sep_idx];
1137    let path = &line[sep_idx.checked_add(sep_len)?..];
1138    let path = std::str::from_utf8(path).ok()?;
1139
1140    // Validate hash is hex and has valid length:
1141    // 64 for SHA256, 128 for SHA512.
1142    let algo: &[u8] = match hash.len() {
1143        64 => b"SHA256",
1144        128 => b"SHA512",
1145        _ => return None,
1146    };
1147    if !hash.iter().all(|&b| b.is_ascii_hexdigit()) {
1148        return None;
1149    }
1150
1151    Some((algo, hash, path))
1152}
1153
1154/// Verify a file's hash against expected value.
1155///
1156/// Used by both BSD and GNU format parsing.
1157fn verify_file_with_hash(algo: &[u8], hash: &[u8], path: &str, quiet: bool) -> Result<()> {
1158    let name = log_untrusted_buf(path.as_bytes());
1159    let path = Path::new(path);
1160
1161    let mut file = open(path, false).inspect_err(|error| {
1162        if !quiet {
1163            println!("{name}: FAIL: {error}");
1164        }
1165    })?;
1166
1167    const BUF_SIZE: usize = 64 * 1024;
1168    let mut buf = [0_u8; BUF_SIZE];
1169
1170    // Macro to compute hash for a given hasher.
1171    macro_rules! compute_hash {
1172        ($hasher:expr) => {{
1173            let mut hasher = $hasher;
1174            loop {
1175                match file.read(&mut buf) {
1176                    Ok(0) => break,
1177                    Ok(n) => hasher.update(&buf[..n]),
1178                    Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1179                    Err(error) => {
1180                        let error = Error::Io(error);
1181                        if !quiet {
1182                            println!("{name}: FAIL: {error}");
1183                        }
1184                        return Err(error);
1185                    }
1186                }
1187            }
1188            HEXLOWER.encode(&hasher.finalize())
1189        }};
1190    }
1191
1192    let calculated_hash = match algo {
1193        b"SHA256" => compute_hash!(Sha256::new()),
1194        b"SHA512" => compute_hash!(Sha512::new()),
1195        _ => {
1196            let algo = String::from_utf8_lossy(algo).to_string();
1197            let error = Error::UnsupportedHashAlgo(algo);
1198            if !quiet {
1199                println!("{name}: FAIL: {error}");
1200            }
1201            return Err(error);
1202        }
1203    };
1204
1205    if calculated_hash.as_bytes().eq_ignore_ascii_case(hash) {
1206        if !quiet {
1207            println!("{name}: OK");
1208        }
1209        Ok(())
1210    } else {
1211        let error = Error::ChecksumMismatch(name);
1212        if !quiet {
1213            println!("{error}");
1214        }
1215        Err(error)
1216    }
1217}
1218
1219/// Helper to read gzip header.
1220fn read_gzip_header(sig_file: &mut dyn Read) -> Result<GzipHeader> {
1221    let mut head = [0u8; 10];
1222    sig_file.read_exact(&mut head).map_err(Error::Io)?;
1223
1224    if head[0] != 0x1f || head[1] != 0x8b {
1225        return Err(Error::Io(std::io::Error::new(
1226            std::io::ErrorKind::InvalidData,
1227            "Not a gzip file",
1228        )));
1229    }
1230    let flg = head[3];
1231    if (flg & 16) == 0 {
1232        return Err(Error::Io(std::io::Error::new(
1233            std::io::ErrorKind::InvalidData,
1234            "Unsigned gzip archive (no comment)",
1235        )));
1236    }
1237
1238    // Skip extra, name if present.
1239    let mut extra_field = Vec::new();
1240    if (flg & 4) != 0 {
1241        let mut xlen_b = [0u8; 2];
1242        sig_file.read_exact(&mut xlen_b).map_err(Error::Io)?;
1243        extra_field.extend_from_slice(&xlen_b);
1244        let xlen = u64::from(u16::from_le_bytes(xlen_b));
1245        let mut reader = sig_file.take(xlen);
1246        reader.read_to_end(&mut extra_field).map_err(Error::Io)?;
1247    }
1248
1249    let mut name_field = Vec::new();
1250    if (flg & 8) != 0 {
1251        let mut buf = [0u8; 1];
1252        loop {
1253            sig_file.read_exact(&mut buf).map_err(Error::Io)?;
1254            name_field.push(buf[0]);
1255            if buf[0] == 0 {
1256                break;
1257            }
1258        }
1259    }
1260
1261    // Read comment.
1262    let mut comment_vec = Vec::new();
1263    let mut buf = [0u8; 1];
1264    loop {
1265        sig_file.read_exact(&mut buf).map_err(Error::Io)?;
1266        if buf[0] == 0 {
1267            break;
1268        }
1269        comment_vec.push(buf[0]);
1270    }
1271
1272    Ok(GzipHeader {
1273        flg,
1274        head,
1275        extra_field,
1276        name_field,
1277        comment_vec,
1278    })
1279}
1280
1281/// Sign a gzip file.
1282pub fn sign_gzip(
1283    seckey: &[u8],
1284    keynum: [u8; 8],
1285    seckey_path: &Path,
1286    msg_path: &Path,
1287    sig_path: &Path,
1288    comment_bytes: &[u8],
1289    msg_file: Option<std::fs::File>,
1290    sig_file: Option<std::fs::File>,
1291) -> Result<()> {
1292    let is_stdout = sig_path.as_os_str() == "-";
1293    let is_stdin = msg_path.as_os_str() == "-";
1294
1295    if is_stdin && msg_file.is_none() {
1296        return Err(Error::Io(std::io::Error::new(
1297            std::io::ErrorKind::InvalidInput,
1298            "Gzip signing requires a regular file input (not stdin)",
1299        )));
1300    }
1301
1302    let mut f = if let Some(f) = msg_file {
1303        f
1304    } else {
1305        open(msg_path, false)?
1306    };
1307
1308    let (head, data_start) = parse_gzip_for_signing(&mut f)?;
1309
1310    // Pass 1: Hash.
1311    let (header_msg, _) = hash_gzip_content(&mut f, data_start, seckey_path)?;
1312
1313    // Sign.
1314    let kp = ed25519_compact::KeyPair {
1315        pk: ed25519_compact::PublicKey::from_slice(&seckey[32..]).map_err(Error::Crypto)?,
1316        sk: ed25519_compact::SecretKey::from_slice(seckey).map_err(Error::Crypto)?,
1317    };
1318    let sig = kp.sk.sign(&header_msg, None);
1319
1320    let sig_comment = make_sig_comment(seckey_path, comment_bytes)?;
1321
1322    let sig_header = {
1323        let sig = Sig {
1324            pkalg: PKALG,
1325            keynum,
1326            sig: sig
1327                .as_ref()
1328                .try_into()
1329                .map_err(|_| Error::InvalidSignatureLength)?, // Safe: Fixed length
1330        };
1331        let encoded = Base64::encode_string(&sig.to_bytes());
1332        let mut h = Vec::new();
1333        h.extend_from_slice(COMMENTHDR.as_bytes());
1334        h.extend_from_slice(&sig_comment);
1335        h.push(b'\n');
1336        h.extend_from_slice(encoded.as_bytes());
1337        h.push(b'\n');
1338        h
1339    };
1340
1341    let mut out: Box<dyn Write> = if let Some(f) = sig_file {
1342        Box::new(f)
1343    } else if is_stdout {
1344        Box::new(stdout())
1345    } else {
1346        Box::new(open(sig_path, true)?)
1347    };
1348
1349    write_signed_gzip(
1350        &mut out,
1351        &head,
1352        &sig_header,
1353        &header_msg,
1354        &mut f,
1355        data_start,
1356    )
1357}
1358
1359fn parse_gzip_for_signing(f: &mut std::fs::File) -> Result<([u8; 10], u64)> {
1360    let mut head = [0u8; 10];
1361    f.read_exact(&mut head).map_err(Error::Io)?;
1362
1363    if head[0] != 0x1f || head[1] != 0x8b {
1364        return Err(Error::Io(std::io::Error::new(
1365            std::io::ErrorKind::InvalidData,
1366            "Not a gzip file",
1367        )));
1368    }
1369
1370    let flg = head[3];
1371    if (flg & 4) != 0 {
1372        let mut xlen_b = [0u8; 2];
1373        f.read_exact(&mut xlen_b).map_err(Error::Io)?;
1374        let xlen = i64::from(u16::from_le_bytes(xlen_b));
1375        f.seek(SeekFrom::Current(xlen)).map_err(Error::Io)?;
1376    }
1377
1378    if (flg & 8) != 0 {
1379        let mut buf = [0u8; 1];
1380        loop {
1381            f.read_exact(&mut buf).map_err(Error::Io)?;
1382            if buf[0] == 0 {
1383                break;
1384            }
1385        }
1386    }
1387
1388    if (flg & 16) != 0 {
1389        let mut buf = [0u8; 1];
1390        loop {
1391            f.read_exact(&mut buf).map_err(Error::Io)?;
1392            if buf[0] == 0 {
1393                break;
1394            }
1395        }
1396    }
1397
1398    if (flg & 2) != 0 {
1399        f.seek(SeekFrom::Current(2)).map_err(Error::Io)?;
1400    }
1401
1402    let data_start = f.stream_position().map_err(Error::Io)?;
1403    Ok((head, data_start))
1404}
1405
1406fn hash_gzip_content(
1407    f: &mut std::fs::File,
1408    data_start: u64,
1409    seckey_path: &Path,
1410) -> Result<(Vec<u8>, usize)> {
1411    const BLK_SIZE: usize = 0x0001_0000;
1412    let mut block = vec![0u8; BLK_SIZE];
1413
1414    // Scan passes.
1415    loop {
1416        match f.read(&mut block) {
1417            Ok(0) => break,
1418            Ok(_) => continue,
1419            Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1420            Err(e) => return Err(Error::Io(e)),
1421        }
1422    }
1423
1424    let mut header_msg = Vec::new();
1425    let time_now = "0000-00-00T00:00:00Z";
1426    let keyname = if seckey_path.as_os_str() == "-" {
1427        "stdin"
1428    } else {
1429        seckey_path.to_str().ok_or(Error::InvalidPath)?
1430    };
1431    write!(
1432        &mut header_msg,
1433        "date={time_now}\nkey={keyname}\nalgorithm=SHA256\nblocksize={BLK_SIZE}\n\n",
1434    )
1435    .map_err(Error::Io)?;
1436
1437    f.seek(SeekFrom::Start(data_start)).map_err(Error::Io)?;
1438    loop {
1439        match f.read(&mut block) {
1440            Ok(0) => break,
1441            Ok(n) => {
1442                let hash = Sha256::digest(&block[..n]);
1443                let hex = HEXLOWER.encode(&hash);
1444                header_msg.extend_from_slice(hex.as_bytes());
1445                header_msg.push(b'\n');
1446            }
1447            Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1448            Err(e) => return Err(Error::Io(e)),
1449        }
1450    }
1451
1452    Ok((header_msg, BLK_SIZE))
1453}
1454
1455fn write_signed_gzip(
1456    out: &mut dyn Write,
1457    head: &[u8; 10],
1458    sig_header: &[u8],
1459    header_msg: &[u8],
1460    input: &mut std::fs::File,
1461    data_start: u64,
1462) -> Result<()> {
1463    let fake_header = [0x1f, 0x8b, 8, 16, 0, 0, 0, 0, head[8], 3];
1464    out.write_all(&fake_header).map_err(Error::Io)?;
1465    out.write_all(sig_header).map_err(Error::Io)?;
1466    out.write_all(header_msg).map_err(Error::Io)?;
1467    out.write_all(&[0u8]).map_err(Error::Io)?;
1468
1469    input.seek(SeekFrom::Start(data_start)).map_err(Error::Io)?;
1470    copy(input, out).map_err(Error::Io)?;
1471    Ok(())
1472}
1473
1474/// Verify a gzip signature.
1475pub fn verify_gzip(
1476    pubkey: Option<&Path>,
1477    msg_path: &Path,
1478    sig_path: &Path,
1479    quiet: bool,
1480    msg_file: Option<File>,
1481    sig_file: Option<File>,
1482    pubkey_file: Option<File>,
1483) -> Result<()> {
1484    let mut sig_file: Box<dyn Read> = if let Some(f) = sig_file {
1485        Box::new(f)
1486    } else if sig_path.as_os_str() == "-" {
1487        Box::new(stdin())
1488    } else {
1489        Box::new(open(sig_path, false)?)
1490    };
1491
1492    let header = read_gzip_header(sig_file.as_mut())?;
1493
1494    // Parse signature from comment.
1495    let (sig, header_list) = parse_sig_from_comment(&header.comment_vec)?;
1496
1497    // Verify signature of header list.
1498    let pubkey = if let Some(f) = pubkey_file {
1499        let (pk, _) = parse_stream(f, PubKey::from_bytes)?;
1500        pk
1501    } else if let Some(path) = pubkey {
1502        let (pk, _) = parse::<PubKey, _>(path, PubKey::from_bytes)?;
1503        pk
1504    } else {
1505        return Err(Error::MissingPubKey);
1506    };
1507
1508    if sig.keynum != pubkey.keynum {
1509        return Err(Error::KeyMismatch);
1510    }
1511    crypto::verify(header_list, &pubkey.pubkey, &sig.sig)?;
1512
1513    // Parse header lines.
1514    let header_str = str::from_utf8(header_list).map_err(|_| Error::InvalidCommentHeader)?;
1515    let mut lines = header_str.lines();
1516    let blocksize = parse_header_metadata(&mut lines)?;
1517
1518    // Output setup.
1519    let mut out_writer: Option<Box<dyn Write>> = if let Some(f) = msg_file {
1520        Some(Box::new(f))
1521    } else if msg_path.as_os_str() == "-" {
1522        Some(Box::new(stdout()))
1523    } else {
1524        Some(Box::new(open(msg_path, true)?))
1525    };
1526
1527    // Write header.
1528    if let Some(w) = out_writer.as_mut() {
1529        w.write_all(&header.head).map_err(Error::Io)?;
1530        if !header.extra_field.is_empty() {
1531            w.write_all(&header.extra_field).map_err(Error::Io)?;
1532        }
1533        if !header.name_field.is_empty() {
1534            w.write_all(&header.name_field).map_err(Error::Io)?;
1535        }
1536        w.write_all(&header.comment_vec).map_err(Error::Io)?;
1537        w.write_all(&[0u8]).map_err(Error::Io)?;
1538    }
1539
1540    if (header.flg & 2) != 0 {
1541        let mut crc = [0u8; 2];
1542        sig_file.read_exact(&mut crc).map_err(Error::Io)?;
1543        if let Some(w) = out_writer.as_mut() {
1544            w.write_all(&crc).map_err(Error::Io)?;
1545        }
1546    }
1547
1548    verify_payload_blocks(sig_file.as_mut(), lines, blocksize, out_writer.as_mut())?;
1549
1550    if !quiet {
1551        eprintln!("Signature Verified");
1552    }
1553    Ok(())
1554}
1555
1556fn parse_sig_from_comment(comment_vec: &[u8]) -> Result<(Sig, &[u8])> {
1557    let n1 = memchr(b'\n', comment_vec).ok_or(Error::InvalidCommentHeader)?;
1558    let n2_start = n1.checked_add(1).ok_or(Error::Overflow)?;
1559    let n2 = memchr(b'\n', &comment_vec[n2_start..]).ok_or(Error::MissingSignatureNewline)?;
1560    let sig_end = n2_start.checked_add(n2).ok_or(Error::Overflow)?;
1561
1562    let header_list = &comment_vec[sig_end.checked_add(1).ok_or(Error::Overflow)?..];
1563
1564    let b64_bytes = &comment_vec[n2_start..sig_end];
1565    let b64_str = str::from_utf8(b64_bytes).map_err(|_e| Error::InvalidSignatureUtf8)?;
1566    let sig_bytes = Base64::decode_vec(b64_str.trim()).map_err(Error::Base64Decode)?;
1567    let sig = Sig::from_bytes(&sig_bytes)?;
1568
1569    Ok((sig, header_list))
1570}
1571
1572fn parse_header_metadata(lines: &mut std::str::Lines) -> Result<usize> {
1573    let mut algo = "SHA256";
1574    let mut blocksize = 0x0001_0000;
1575
1576    for l in lines.by_ref() {
1577        if l.is_empty() {
1578            break;
1579        }
1580        if let Some(val) = l.strip_prefix("algorithm=") {
1581            algo = val;
1582        } else if let Some(val) = l.strip_prefix("blocksize=") {
1583            blocksize = val.parse().unwrap_or(0x0001_0000);
1584        }
1585    }
1586
1587    if algo != "SHA256" && algo != "SHA512/256" {
1588        return Err(Error::Io(std::io::Error::new(
1589            std::io::ErrorKind::InvalidData,
1590            format!("Unsupported algorithm: {algo}"),
1591        )));
1592    }
1593    Ok(blocksize)
1594}
1595
1596fn verify_payload_blocks(
1597    sig_file: &mut dyn Read,
1598    lines: std::str::Lines,
1599    blocksize: usize,
1600    mut out_writer: Option<&mut Box<dyn Write>>,
1601) -> Result<()> {
1602    let mut buf = vec![0u8; blocksize];
1603    for hash_line in lines {
1604        let n = loop {
1605            match sig_file.read(&mut buf) {
1606                Ok(n) => break n,
1607                Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1608                Err(e) => return Err(Error::Io(e)),
1609            }
1610        };
1611
1612        if n == 0 {
1613            return Err(Error::Io(std::io::Error::new(
1614                std::io::ErrorKind::UnexpectedEof,
1615                "Premature end of archive",
1616            )));
1617        }
1618        let hash = Sha256::digest(&buf[..n]);
1619        let hash_hex = HEXLOWER.encode(&hash);
1620
1621        if hash_hex != hash_line {
1622            return Err(Error::VerifyFailed);
1623        }
1624        if let Some(w) = out_writer.as_mut() {
1625            w.write_all(&buf[..n]).map_err(Error::Io)?;
1626        }
1627    }
1628
1629    // Ensure EOF.
1630    let n = loop {
1631        match sig_file.read(&mut buf) {
1632            Ok(n) => break n,
1633            Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
1634            Err(e) => return Err(Error::Io(e)),
1635        }
1636    };
1637    if n != 0 {
1638        return Err(Error::Io(std::io::Error::new(
1639            std::io::ErrorKind::InvalidData,
1640            "Trailing data in archive",
1641        )));
1642    }
1643    Ok(())
1644}
1645
1646fn make_sig_comment(seckey_path: &Path, comment_bytes: &[u8]) -> Result<Vec<u8>> {
1647    let mut sig_comment = Vec::new();
1648    if seckey_path.as_os_str() == "-" {
1649        sig_comment.extend_from_slice(b"signature from ");
1650        sig_comment.extend_from_slice(comment_bytes);
1651    } else {
1652        let basename = check_keyname_compliance(None, seckey_path)?;
1653        sig_comment.extend_from_slice(b"verify with ");
1654        sig_comment.extend_from_slice(basename.as_bytes());
1655        sig_comment.extend_from_slice(b".pub");
1656    };
1657    Ok(sig_comment)
1658}
1659
1660#[cfg(test)]
1661mod tests {
1662    use super::*;
1663    use std::path::PathBuf;
1664
1665    #[test]
1666    fn test_signer_default() {
1667        let signer = Signer::default();
1668        assert_eq!(signer.seckey, None);
1669        assert!(!signer.embed);
1670        assert!(!signer.gzip);
1671        assert_eq!(signer.key_id, None);
1672    }
1673
1674    #[test]
1675    fn test_signer_builder() {
1676        let path = PathBuf::from("test.sec");
1677        let signer = Signer::new()
1678            .seckey(path.clone())
1679            .embed(true)
1680            .gzip(true)
1681            .key_id(42);
1682
1683        assert_eq!(signer.seckey, Some(path));
1684        assert!(signer.embed);
1685        assert!(signer.gzip);
1686        assert_eq!(signer.key_id, Some(42));
1687    }
1688
1689    #[test]
1690    fn test_verifier_default() {
1691        let verifier = Verifier::default();
1692        assert_eq!(verifier.pubkey, None);
1693        assert!(!verifier.quiet);
1694        assert!(!verifier.embed);
1695        assert!(!verifier.gzip);
1696    }
1697
1698    #[test]
1699    fn test_verifier_builder() {
1700        let path = PathBuf::from("test.pub");
1701        let verifier = Verifier::new()
1702            .pubkey(path.clone())
1703            .quiet(true)
1704            .embed(true)
1705            .gzip(true);
1706
1707        assert_eq!(verifier.pubkey, Some(path));
1708        assert!(verifier.quiet);
1709        assert!(verifier.embed);
1710        assert!(verifier.gzip);
1711    }
1712}