libsignify_rs/
ops.rs

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