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