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::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
45/// Gzip header structure.
46struct GzipHeader {
47    /// Gzip flags.
48    flg: u8,
49    /// Header bytes (magic, compression method, flags, mtime, xfl, os).
50    head: [u8; 10],
51    /// Extra field data.
52    extra_field: Vec<u8>,
53    /// Original filename field.
54    name_field: Vec<u8>,
55    /// Comment field.
56    comment_vec: Vec<u8>,
57}
58
59/// Builder for key generation operations.
60///
61/// # Examples
62///
63/// ```rust
64/// use libsignify_rs::KeyGenerator;
65/// use tempfile::tempdir;
66///
67/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
68/// let dir = tempdir()?;
69/// let pub_path = dir.path().join("key.pub");
70/// let sec_path = dir.path().join("key.sec");
71///
72/// // Generate with default settings (secure KDF rounds)
73/// KeyGenerator::new()
74///     .rounds(0) // No encryption for automated testing
75///     .comment("test-key")
76///     .generate(&pub_path, &sec_path)?;
77///
78/// assert!(pub_path.exists());
79/// assert!(sec_path.exists());
80/// # Ok(())
81/// # }
82/// ```
83#[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    /// Create a new `KeyGenerator` with default settings.
93    #[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    /// Set TTY handle for interactive password prompts.
104    #[must_use]
105    pub fn tty_handle(mut self, tty: File) -> Self {
106        self.tty_handle = Some(tty);
107        self
108    }
109
110    /// Set KDF rounds (0 for no encryption).
111    ///
112    /// Default is 42.
113    #[must_use]
114    pub fn rounds(mut self, rounds: u32) -> Self {
115        self.rounds = rounds;
116        self
117    }
118
119    /// Set key comment.
120    ///
121    /// Default is "signify key".
122    #[must_use]
123    pub fn comment(mut self, comment: impl Into<String>) -> Self {
124        self.comment = Some(comment.into());
125        self
126    }
127
128    /// Set key ID for keyring integration.
129    #[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    /// Execute key generation writing to streams.
136    ///
137    /// # Errors
138    /// Returns errors if I/O or KDF fails.
139    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        // Write secret key.
202        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    /// Execute key generation.
218    ///
219    /// # Errors
220    /// Returns errors if file I/O or KDF fails.
221    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
228/// Builder for signing operations.
229///
230/// # Examples
231///
232/// Basic signing:
233///
234/// ```rust
235/// use libsignify_rs::{KeyGenerator, Signer};
236/// use tempfile::tempdir;
237/// use std::fs;
238///
239/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
240/// let dir = tempdir()?;
241/// let pub_path = dir.path().join("key.pub");
242/// let sec_path = dir.path().join("key.sec");
243/// let msg_path = dir.path().join("msg.txt");
244/// let sig_path = dir.path().join("msg.sig");
245///
246/// // Setup: Generate keys and message
247/// KeyGenerator::new().rounds(0).generate(&pub_path, &sec_path)?;
248/// fs::write(&msg_path, "test message")?;
249///
250/// // Sign
251/// Signer::new()
252///     .seckey(&sec_path)
253///     .sign(&msg_path, &sig_path)?;
254///
255/// assert!(sig_path.exists());
256/// # Ok(())
257/// # }
258/// ```
259///
260/// Embedded signature:
261///
262/// ```rust
263/// # use libsignify_rs::{KeyGenerator, Signer};
264/// # use tempfile::tempdir;
265/// # use std::fs;
266/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
267/// # let dir = tempdir()?;
268/// # let pub_path = dir.path().join("key.pub");
269/// # let sec_path = dir.path().join("key.sec");
270/// # let msg_path = dir.path().join("msg.txt");
271/// # let sig_path = dir.path().join("msg.sig");
272/// # KeyGenerator::new().rounds(0).generate(&pub_path, &sec_path)?;
273/// # fs::write(&msg_path, "test message")?;
274/// Signer::new()
275///     .seckey(&sec_path)
276///     .embed(true)
277///     .sign(&msg_path, &sig_path)?;
278/// # Ok(())
279/// # }
280/// ```
281///
282/// Gzip signing:
283///
284/// ```rust
285/// # use libsignify_rs::{KeyGenerator, Signer};
286/// # use tempfile::tempdir;
287/// # use std::fs;
288/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
289/// # let dir = tempdir()?;
290/// # let pub_path = dir.path().join("key.pub");
291/// # let sec_path = dir.path().join("key.sec");
292/// # let msg_path = dir.path().join("archive.gz"); // Input must be a file
293/// # let sig_path = dir.path().join("archive.gz.sig"); // Output is signed gzip
294/// # KeyGenerator::new().rounds(0).generate(&pub_path, &sec_path)?;
295/// use flate2::write::GzEncoder;
296/// use flate2::Compression;
297/// use std::fs::File;
298/// use std::io::Write;
299///
300/// // Create a valid gzip file to sign
301/// let f = File::create(&msg_path)?;
302/// let mut e = GzEncoder::new(f, Compression::default());
303/// e.write_all(b"compressed content")?;
304/// e.finish()?;
305///
306/// Signer::new()
307///     .seckey(&sec_path)
308///     .gzip(true)
309///     .sign(&msg_path, &sig_path)?;
310/// # Ok(())
311/// # }
312/// ```
313pub 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    /// Create a new `Signer`.
329    #[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    /// Set TTY handle for interactive password prompts.
341    #[must_use]
342    pub fn tty_handle(mut self, tty: File) -> Self {
343        self.tty_handle = Some(tty);
344        self
345    }
346
347    /// Set secret key path.
348    #[must_use]
349    pub fn seckey(mut self, path: impl Into<PathBuf>) -> Self {
350        self.seckey = Some(path.into());
351        self
352    }
353
354    /// Set embed mode (signify -e).
355    ///
356    /// If true, creates an embedded signature.
357    #[must_use]
358    pub fn embed(mut self, embed: bool) -> Self {
359        self.embed = embed;
360        self
361    }
362
363    /// Set gzip mode (signify -z).
364    ///
365    /// If true, signs a gzip archive.
366    #[must_use]
367    pub fn gzip(mut self, gzip: bool) -> Self {
368        self.gzip = gzip;
369        self
370    }
371
372    /// Set key ID (for keyring support).
373    #[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    /// Sign a file.
380    ///
381    /// # Errors
382    /// Returns errors if I/O, decryption, or signing fails.
383    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    /// Sign a message using optional file handles.
388    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]); // 64
418        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        // Verify checksum to ensure the password was correct.
427        let checksum = derive_checksum(seckey.as_ref());
428        if checksum != enc_key.checksum {
429            return Err(Error::IncorrectPassphrase);
430        }
431
432        // Determine paths and streams.
433        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        // Pre-calculate signature comment (used in headers).
456        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
486/// Builder for verification operations.
487///
488/// # Examples
489///
490/// Basic verification:
491///
492/// ```rust
493/// use libsignify_rs::{KeyGenerator, Signer, Verifier};
494/// use tempfile::tempdir;
495/// use std::fs;
496///
497/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
498/// let dir = tempdir()?;
499/// let pub_path = dir.path().join("key.pub");
500/// let sec_path = dir.path().join("key.sec");
501/// let msg_path = dir.path().join("msg.txt");
502/// let sig_path = dir.path().join("msg.sig");
503///
504/// // Setup
505/// KeyGenerator::new().rounds(0).generate(&pub_path, &sec_path)?;
506/// fs::write(&msg_path, "test message")?;
507/// Signer::new().seckey(&sec_path).sign(&msg_path, &sig_path)?;
508///
509/// // Verify
510/// Verifier::new()
511///     .pubkey(&pub_path)
512///     .quiet(true)
513///     .verify(&msg_path, &sig_path)?;
514/// # Ok(())
515/// # }
516/// ```
517///
518/// Embedded verification:
519///
520/// ```rust
521/// # use libsignify_rs::{KeyGenerator, Signer, Verifier};
522/// # use tempfile::tempdir;
523/// # use std::fs;
524/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
525/// # let dir = tempdir()?;
526/// # let pub_path = dir.path().join("key.pub");
527/// # let sec_path = dir.path().join("key.sec");
528/// # let msg_path = dir.path().join("msg.txt");
529/// # let sig_path = dir.path().join("msg.sig");
530/// # KeyGenerator::new().rounds(0).generate(&pub_path, &sec_path)?;
531/// # fs::write(&msg_path, "test message")?;
532/// // Sign with embed
533/// Signer::new()
534///     .seckey(&sec_path)
535///     .embed(true)
536///     .sign(&msg_path, &sig_path)?;
537///
538/// // Remove original message to verify extraction
539/// fs::remove_file(&msg_path)?;
540///
541/// // Verify embedded
542/// Verifier::new()
543///     .pubkey(&pub_path)
544///     .quiet(true)
545///     .embed(true)
546///     .verify(&msg_path, &sig_path)?;
547/// # Ok(())
548/// # }
549/// ```
550///
551/// Gzip verification:
552///
553/// ```rust
554/// # use libsignify_rs::{KeyGenerator, Signer, Verifier};
555/// # use tempfile::tempdir;
556/// # use std::fs::File;
557/// # use std::path::Path;
558/// # use flate2::{write::GzEncoder, Compression};
559/// # use std::io::Write;
560/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
561/// # let dir = tempdir()?;
562/// # let pub_path = dir.path().join("key.pub");
563/// # let sec_path = dir.path().join("key.sec");
564/// # let msg_path = dir.path().join("archive.gz");
565/// # let sig_path = dir.path().join("archive.gz.sig");
566/// # KeyGenerator::new().rounds(0).generate(&pub_path, &sec_path)?;
567/// # let f = File::create(&msg_path)?;
568/// # let mut e = GzEncoder::new(f, Compression::default());
569/// # e.write_all(b"data")?;
570/// # e.finish()?;
571/// // Sign gzip
572/// Signer::new()
573///     .seckey(&sec_path)
574///     .gzip(true)
575///     .sign(&msg_path, &sig_path)?;
576///
577/// // Verify gzip
578/// Verifier::new()
579///     .pubkey(&pub_path)
580///     .quiet(true)
581///     .gzip(true)
582///     .verify(&Path::new("-"), &sig_path)?; // Output to stdout (-) or file
583/// # Ok(())
584/// # }
585/// ```
586///
587/// Handling verification failures:
588///
589/// ```rust
590/// # use libsignify_rs::{KeyGenerator, Signer, Verifier};
591/// # use tempfile::tempdir;
592/// # use std::fs;
593/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
594/// # let dir = tempdir()?;
595/// # let pub_path = dir.path().join("key.pub");
596/// # let sec_path = dir.path().join("key.sec");
597/// # let msg_path = dir.path().join("msg.txt");
598/// # let sig_path = dir.path().join("msg.sig");
599/// # KeyGenerator::new().rounds(0).generate(&pub_path, &sec_path)?;
600/// # fs::write(&msg_path, "original message")?;
601/// # Signer::new().seckey(&sec_path).sign(&msg_path, &sig_path)?;
602///
603/// // Tamper with the message
604/// fs::write(&msg_path, "tampered message")?;
605///
606/// // Verification should fail
607/// let result = Verifier::new()
608///     .pubkey(&pub_path)
609///     .verify(&msg_path, &sig_path);
610///
611/// assert!(result.is_err());
612/// # Ok(())
613/// # }
614/// ```
615pub 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    /// Create a new `Verifier`.
630    #[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    /// Set public key path.
641    #[must_use]
642    pub fn pubkey(mut self, path: impl Into<PathBuf>) -> Self {
643        self.pubkey = Some(path.into());
644        self
645    }
646
647    /// Set quiet mode.
648    ///
649    /// If true, suppresses "Signature Verified" output.
650    #[must_use]
651    pub fn quiet(mut self, quiet: bool) -> Self {
652        self.quiet = quiet;
653        self
654    }
655
656    /// Set embed mode (signify -I).
657    ///
658    /// Used when verifying an embedded signature.
659    #[must_use]
660    pub fn embed(mut self, embed: bool) -> Self {
661        self.embed = embed;
662        self
663    }
664
665    /// Set gzip mode (signify -z).
666    ///
667    /// Used when verifying a signed gzip archive.
668    #[must_use]
669    pub fn gzip(mut self, gzip: bool) -> Self {
670        self.gzip = gzip;
671        self
672    }
673
674    /// Verify a signature.
675    ///
676    /// # Errors
677    /// Returns errors if verification fails.
678    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    /// Verify with pre-opened resources.
683    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        // Standard embedded/detached verify:
704        // Determine signature and input stream.
705        let (sig, stream, output_path, comment_opt) = if self.embed {
706            // Embed mode: msg_path is output (extracted msg).
707            // Sig is input. extract signature from sig_path/sig_file.
708            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            // Detached
719            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            // Detached verification never extracts/writes.
733            (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            // Try autolocate using comment_opt or reading file header.
744            let comment = if let Some(comment) = comment_opt {
745                comment
746            } else {
747                // Re-read header for autolocate if needed.
748                // If sig_file was a pipe and consumed, we can't re-read.
749                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        // Prepare output writer if needed.
759        let mut writer: Option<Box<dyn Write>> = if let Some(f) = msg_file.take() {
760            // msg_file provided. Used for output if embed?
761            Some(Box::new(f))
762        } else if let Some(path) = output_path {
763            // Standard path open.
764            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        // Verify stream.
774        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    // Read header bounded.
787    const HEADER_LIMIT: usize = 4096;
788    let mut buffer = vec![0_u8; HEADER_LIMIT];
789    let mut valid_len = 0;
790
791    // Read up to limit.
792    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    // Construct prelude: (buffer[msg_start..]).
819    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
828/// Prompt for password.
829fn 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
847/// Parameters for signing operations.
848struct SignParams<'a> {
849    /// Secret key bytes.
850    seckey: &'a [u8; 64],
851    /// Key ID.
852    keynum: [u8; 8],
853    /// Path to the message file.
854    msg_path: &'a Path,
855    /// Path to the signature file.
856    sig_path: &'a Path,
857    /// Whether to embed the signature.
858    embed: bool,
859    /// Whether output is stdout.
860    is_stdout: bool,
861    /// Whether input is stdin.
862    is_stdin: bool,
863    /// Signature comment to include.
864    sig_comment: &'a [u8],
865    /// Pre-opened message file.
866    msg_file: Option<File>,
867    /// Pre-opened signature file.
868    sig_file: Option<File>,
869}
870
871fn sign_standard(params: SignParams) -> Result<()> {
872    // Helper to create header bytes from signature bytes.
873    let make_header = |sig_bytes: &[u8]| -> Result<Vec<u8>> {
874        // Unwrap is safe because it's fixed length.
875        #[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            // File output: Seek and patch.
902            // 1. Write dummy header.
903            let dummy_sig = [0u8; 64];
904            let header = make_header(&dummy_sig)?;
905            file.write_all(&header).map_err(Error::Io)?;
906
907            // 2. Stream input.
908            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            // 3. Patch.
921            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            // Output is stdout.
930            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                // File input: Two pass (read hash, read write).
942                let mut file = if let Some(file) = params.msg_file {
943                    file
944                } else {
945                    open(params.msg_path, false)?
946                };
947
948                // Ensure start
949                // If it's a file, seek to start.
950                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                // Rewind.
958                file.rewind().map_err(Error::Io)?;
959                copy(&mut file, &mut out).map_err(Error::Io)?;
960            }
961        }
962    } else {
963        // Detached (-S)
964        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
984/// Autolocate helper
985/// Autolocate a key from a signature comment.
986pub(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
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: &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/// Verify a single line from a checksum file.
1067#[must_use]
1068pub fn verify_checksum_line(line: &[u8], quiet: bool) -> bool {
1069    // Parse "ALGO (FILENAME) = HASH" -> Find " = ".
1070    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    // Parse left: "ALGO (FILENAME)" -> Find first space.
1083    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    // Rest should be "(FILENAME)"
1093    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    // Use 64KB buffer.
1116    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
1179/// Helper to read gzip header.
1180fn 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    // Skip extra, name if present.
1199    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    // Read comment.
1222    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
1241/// Sign a gzip file.
1242pub 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    // Pass 1: Hash.
1271    let (header_msg, _) = hash_gzip_content(&mut f, data_start, seckey_path)?;
1272
1273    // Sign.
1274    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)?, // Safe: Fixed length
1290        };
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    // Scan passes.
1375    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
1434/// Verify a gzip signature.
1435pub 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    // Parse signature from comment.
1455    let (sig, header_list) = parse_sig_from_comment(&header.comment_vec)?;
1456
1457    // Verify signature of header list.
1458    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    // Parse header lines.
1474    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    // Output setup.
1479    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    // Write header.
1488    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    // Ensure EOF.
1590    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}