minisign_verify/
lib.rs

1//! A small crate to verify [Minisign](https://jedisct1.github.io/minisign/) signatures.
2//!
3//! This library provides zero-dependency verification of Minisign signatures. Minisign is a
4//! dead simple tool to sign files and verify signatures, developed by Frank Denis
5//! (author of libsodium).
6//!
7//! ## Features
8//!
9//! * Verify signatures for both standard and pre-hashed modes
10//! * Streaming verification for large files
11//! * No external dependencies
12//! * Simple, auditable code
13//!
14//! ## Basic Usage
15//!
16//! ```rust
17//! use minisign_verify::{PublicKey, Signature};
18//!
19//! // Create a public key from a base64 string
20//! let public_key =
21//!     PublicKey::from_base64("RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3")
22//!         .expect("Unable to decode the public key");
23//!
24//! // Create a signature from a string
25//! let signature = Signature::decode(
26//!     "untrusted comment: signature from minisign secret key
27//! RUQf6LRCGA9i559r3g7V1qNyJDApGip8MfqcadIgT9CuhV3EMhHoN1mGTkUidF/\
28//!      z7SrlQgXdy8ofjb7bNJJylDOocrCo8KLzZwo=
29//! trusted comment: timestamp:1633700835\tfile:test\tprehashed
30//! wLMDjy9FLAuxZ3q4NlEvkgtyhrr0gtTu6KC4KBJdITbbOeAi1zBIYo0v4iTgt8jJpIidRJnp94ABQkJAgAooBQ==",
31//! )
32//! .expect("Unable to decode the signature");
33//!
34//! // Verify the signature
35//! let bin = b"test";
36//! public_key
37//!     .verify(&bin[..], &signature, false)
38//!     .expect("Signature didn't verify");
39//! ```
40//!
41//! ## Loading from Files
42//!
43//! ```rust,no_run
44//! use minisign_verify::{PublicKey, Signature};
45//! use std::path::Path;
46//!
47//! // Load a public key from a file
48//! let public_key = PublicKey::from_file(Path::new("minisign.pub"))
49//!     .expect("Unable to load the public key");
50//!
51//! // Load a signature from a file
52//! let signature = Signature::from_file(Path::new("file.sig"))
53//!     .expect("Unable to load the signature");
54//!
55//! // Load the file content to verify
56//! let content = std::fs::read("file").expect("Unable to read the file");
57//!
58//! // Verify the signature
59//! public_key
60//!     .verify(&content, &signature, false)
61//!     .expect("Signature didn't verify");
62//! ```
63//!
64//! ## Streaming Verification
65//!
66//! For large files, you can use streaming verification to avoid loading
67//! the entire file into memory at once:
68//!
69//! ```rust,no_run
70//! use minisign_verify::{PublicKey, Signature};
71//! use std::fs::File;
72//! use std::io::{self, Read};
73//! use std::path::Path;
74//!
75//! // Load a public key and signature
76//! let public_key = PublicKey::from_file(Path::new("minisign.pub"))
77//!     .expect("Unable to load the public key");
78//!
79//! let signature = Signature::from_file(Path::new("large_file.sig"))
80//!     .expect("Unable to load the signature");
81//!
82//! // Create a stream verifier
83//! let mut verifier = public_key.verify_stream(&signature)
84//!     .expect("Unable to create stream verifier");
85//!
86//! // Process the file in chunks
87//! let mut file = File::open("large_file").expect("Unable to open file");
88//! let mut buffer = [0u8; 8192]; // 8KB buffer
89//!
90//! loop {
91//!     let bytes_read = file.read(&mut buffer).expect("Error reading file");
92//!     if bytes_read == 0 {
93//!         break; // End of file
94//!     }
95//!
96//!     verifier.update(&buffer[..bytes_read]);
97//! }
98//!
99//! // Verify the signature
100//! verifier.finalize().expect("Signature verification failed");
101//! ```
102//!
103//! Note that the streaming verification mode only works with pre-hashed signatures
104//! (the default in newer versions of Minisign).
105
106mod base64;
107mod crypto;
108
109use std::path::Path;
110use std::{fmt, fs, io};
111
112use base64::{Base64, Decoder};
113
114use crate::crypto::blake2b::{Blake2b, BLAKE2B_OUTBYTES};
115use crate::crypto::ed25519;
116#[derive(Debug)]
117pub enum Error {
118    /// The provided string couldn't be decoded properly
119    InvalidEncoding,
120    /// The signature verification failed
121    InvalidSignature,
122    /// An I/O error occurred
123    IoError(io::Error),
124    /// The algorithm doesn't match what was expected
125    UnexpectedAlgorithm,
126    /// The key ID from the signature doesn't match the public key
127    UnexpectedKeyId,
128    /// The specified algorithm is not supported by this implementation
129    UnsupportedAlgorithm,
130    /// Legacy mode is not supported in streaming verification
131    UnsupportedLegacyMode,
132}
133
134impl fmt::Display for Error {
135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136        match self {
137            Error::InvalidEncoding => write!(f, "Invalid encoding in minisign data"),
138            Error::InvalidSignature => write!(f, "The signature verification failed"),
139            Error::IoError(e) => write!(f, "I/O error: {}", e),
140            Error::UnexpectedAlgorithm => write!(f, "Unexpected signature algorithm"),
141            Error::UnexpectedKeyId => write!(
142                f,
143                "The signature was created with a different key than the one provided"
144            ),
145            Error::UnsupportedAlgorithm => write!(
146                f,
147                "This signature algorithm is not supported by this implementation"
148            ),
149            Error::UnsupportedLegacyMode => {
150                write!(f, "StreamVerifier only supports non-legacy mode signatures")
151            }
152        }
153    }
154}
155
156impl std::error::Error for Error {
157    // Note: description() is deprecated in favor of Display implementation
158
159    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
160        match self {
161            Error::IoError(e) => Some(e),
162            _ => None,
163        }
164    }
165}
166
167impl From<base64::Error> for Error {
168    fn from(_e: base64::Error) -> Error {
169        // We could consider adding a dedicated Base64Error variant that includes the original error
170        // in the future, but for now we'll just use InvalidEncoding
171        Error::InvalidEncoding
172    }
173}
174
175impl From<io::Error> for Error {
176    fn from(e: io::Error) -> Error {
177        Error::IoError(e)
178    }
179}
180
181/// A Minisign public key
182///
183/// This struct represents a Minisign public key, which can be used to verify
184/// signatures. A public key can be created from a base64 string, read from a file,
185/// or parsed from a string in minisign.pub format.
186///
187/// The public key contains an Ed25519 key, a key ID for signature matching,
188/// and optionally an untrusted comment.
189#[derive(Clone, Debug, Eq, PartialEq)]
190pub struct PublicKey {
191    untrusted_comment: Option<String>,
192    signature_algorithm: [u8; 2],
193    key_id: [u8; 8],
194    key: [u8; 32],
195}
196
197/// A StreamVerifier to verify a signature against a data stream
198///
199/// This mode of operation allows for verification of large files by processing them
200/// in chunks, without having to load the entire file into memory.
201///
202/// Note that this mode only works with pre-hashed signatures (not legacy mode).
203#[derive(Clone)]
204pub struct StreamVerifier<'a> {
205    public_key: &'a PublicKey,
206    signature: &'a Signature,
207    hasher: Blake2b,
208}
209
210/// A Minisign signature
211///
212/// This struct represents a Minisign signature, which contains:
213/// - An untrusted comment (usually identifies the key that created the signature)
214/// - The signature itself (Ed25519 signature of the message or its hash)
215/// - A trusted comment (usually contains timestamp and filename)
216/// - A global signature (Ed25519 signature of the signature and trusted comment)
217/// - A flag indicating if the signature was created in pre-hashed mode
218///
219/// Pre-hashed mode is the default in newer versions of Minisign.
220#[derive(Clone)]
221pub struct Signature {
222    untrusted_comment: String,
223    key_id: [u8; 8],
224    signature: [u8; 64],
225    trusted_comment: String,
226    global_signature: [u8; 64],
227    is_prehashed: bool,
228}
229
230impl Signature {
231    /// Create a Minisign signature from a string
232    pub fn decode(lines_str: &str) -> Result<Self, Error> {
233        let mut lines = lines_str.lines();
234        let untrusted_comment = lines.next().ok_or(Error::InvalidEncoding)?.to_string();
235        let bin1 = Base64::decode_to_vec(lines.next().ok_or(Error::InvalidEncoding)?)?;
236        if bin1.len() != 74 {
237            return Err(Error::InvalidEncoding);
238        }
239        let trusted_comment = lines.next().ok_or(Error::InvalidEncoding)?.to_string();
240        let bin2 = Base64::decode_to_vec(lines.next().ok_or(Error::InvalidEncoding)?)?;
241        if bin2.len() != 64 {
242            return Err(Error::InvalidEncoding);
243        }
244        if !trusted_comment.starts_with("trusted comment: ") {
245            return Err(Error::InvalidEncoding);
246        }
247        let mut signature_algorithm = [0u8; 2];
248        signature_algorithm.copy_from_slice(&bin1[0..2]);
249        let mut key_id = [0u8; 8];
250        key_id.copy_from_slice(&bin1[2..10]);
251        let mut signature = [0u8; 64];
252        signature.copy_from_slice(&bin1[10..74]);
253        let mut global_signature = [0u8; 64];
254        global_signature.copy_from_slice(&bin2);
255        let is_prehashed = match (signature_algorithm[0], signature_algorithm[1]) {
256            (0x45, 0x64) => false,
257            (0x45, 0x44) => true,
258            _ => return Err(Error::UnsupportedAlgorithm),
259        };
260        Ok(Signature {
261            untrusted_comment,
262            key_id,
263            signature,
264            trusted_comment,
265            global_signature,
266            is_prehashed,
267        })
268    }
269
270    /// Load a Minisign signature from a `.sig` file
271    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
272        let bin = fs::read_to_string(path)?;
273        Signature::decode(&bin)
274    }
275
276    /// Return the trusted comment of the signature
277    pub fn trusted_comment(&self) -> &str {
278        &self.trusted_comment[17..]
279    }
280
281    /// Return the untrusted comment of the signature
282    pub fn untrusted_comment(&self) -> &str {
283        &self.untrusted_comment
284    }
285}
286
287impl PublicKey {
288    /// Create a Minisign public key from a base64 string
289    pub fn from_base64(public_key_b64: &str) -> Result<Self, Error> {
290        let bin = Base64::decode_to_vec(public_key_b64)?;
291        if bin.len() != 42 {
292            return Err(Error::InvalidEncoding);
293        }
294        let mut signature_algorithm = [0u8; 2];
295        signature_algorithm.copy_from_slice(&bin[0..2]);
296        match (signature_algorithm[0], signature_algorithm[1]) {
297            (0x45, 0x64) | (0x45, 0x44) => {}
298            _ => return Err(Error::UnsupportedAlgorithm),
299        };
300        let mut key_id = [0u8; 8];
301        key_id.copy_from_slice(&bin[2..10]);
302        let mut key = [0u8; 32];
303        key.copy_from_slice(&bin[10..42]);
304        Ok(PublicKey {
305            untrusted_comment: None,
306            signature_algorithm,
307            key_id,
308            key,
309        })
310    }
311
312    /// Create a Minisign public key from a string, as in the `minisign.pub`
313    /// file
314    pub fn decode(lines_str: &str) -> Result<Self, Error> {
315        let mut lines = lines_str.lines();
316        let untrusted_comment = lines.next().ok_or(Error::InvalidEncoding)?;
317        let public_key_b64 = lines.next().ok_or(Error::InvalidEncoding)?;
318        let mut public_key = PublicKey::from_base64(public_key_b64)?;
319        public_key.untrusted_comment = Some(untrusted_comment.to_string());
320        Ok(public_key)
321    }
322
323    /// Load a Minisign key from a file (such as the `minisign.pub` file)
324    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
325        let bin = fs::read_to_string(path)?;
326        PublicKey::decode(&bin)
327    }
328
329    /// Return the untrusted comment, if there is one
330    pub fn untrusted_comment(&self) -> Option<&str> {
331        self.untrusted_comment.as_deref()
332    }
333
334    fn verify_ed25519(&self, bin: &[u8], signature: &Signature) -> Result<(), Error> {
335        if !ed25519::verify(bin, &self.key, &signature.signature) {
336            return Err(Error::InvalidSignature);
337        }
338        let trusted_comment_bin = signature.trusted_comment().as_bytes();
339        let mut global = Vec::with_capacity(signature.signature.len() + trusted_comment_bin.len());
340        global.extend_from_slice(&signature.signature[..]);
341        global.extend_from_slice(trusted_comment_bin);
342        if !ed25519::verify(&global, &self.key, &signature.global_signature) {
343            return Err(Error::InvalidSignature);
344        }
345        Ok(())
346    }
347
348    /// Verify that `signature` is a valid signature for `bin` using this public
349    /// key `allow_legacy` should only be set to `true` in order to support
350    /// signatures made by older versions of Minisign.
351    pub fn verify(
352        &self,
353        bin: &[u8],
354        signature: &Signature,
355        allow_legacy: bool,
356    ) -> Result<(), Error> {
357        if self.key_id != signature.key_id {
358            return Err(Error::UnexpectedKeyId);
359        }
360        let mut h;
361        let bin = if signature.is_prehashed {
362            h = vec![0u8; BLAKE2B_OUTBYTES];
363            Blake2b::blake2b(&mut h, bin);
364            &h
365        } else if !allow_legacy {
366            return Err(Error::UnexpectedAlgorithm);
367        } else {
368            bin
369        };
370        self.verify_ed25519(bin, signature)
371    }
372
373    /// Sets up a stream verifier that can be use iteratively.
374    pub fn verify_stream<'a>(
375        &'a self,
376        signature: &'a Signature,
377    ) -> Result<StreamVerifier<'a>, Error> {
378        if self.key_id != signature.key_id {
379            return Err(Error::UnexpectedKeyId);
380        }
381        if !signature.is_prehashed {
382            return Err(Error::UnsupportedLegacyMode);
383        }
384        let hasher = Blake2b::new(BLAKE2B_OUTBYTES);
385        Ok(StreamVerifier {
386            public_key: self,
387            signature,
388            hasher,
389        })
390    }
391}
392
393impl StreamVerifier<'_> {
394    /// Update the verifier with a chunk of data
395    ///
396    /// This method can be called multiple times with different chunks of the file
397    /// to be verified. The chunks will be hashed incrementally.
398    pub fn update(&mut self, buf: &[u8]) {
399        self.hasher.update(buf);
400    }
401
402    /// Finalize the verification process
403    ///
404    /// This method must be called after all data has been processed with `update()`.
405    /// It computes the final hash and verifies the signature.
406    ///
407    /// Returns `Ok(())` if the signature is valid, or an error otherwise.
408    pub fn finalize(&mut self) -> Result<(), Error> {
409        let mut bin = vec![0u8; BLAKE2B_OUTBYTES];
410        self.hasher.finalize(&mut bin);
411        self.public_key.verify_ed25519(&bin, self.signature)
412    }
413}
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418    #[test]
419    fn verify() {
420        let public_key =
421            PublicKey::from_base64("RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3")
422                .expect("Unable to decode the public key");
423        assert_eq!(public_key.untrusted_comment(), None);
424        let signature = Signature::decode(
425            "untrusted comment: signature from minisign secret key
426RWQf6LRCGA9i59SLOFxz6NxvASXDJeRtuZykwQepbDEGt87ig1BNpWaVWuNrm73YiIiJbq71Wi+dP9eKL8OC351vwIasSSbXxwA=
427trusted comment: timestamp:1555779966\tfile:test
428QtKMXWyYcwdpZAlPF7tE2ENJkRd1ujvKjlj1m9RtHTBnZPa5WKU5uWRs5GoP5M/VqE81QFuMKI5k/SfNQUaOAA==",
429        )
430        .expect("Unable to decode the signature");
431        assert_eq!(
432            signature.untrusted_comment(),
433            "untrusted comment: signature from minisign secret key"
434        );
435        assert_eq!(
436            signature.trusted_comment(),
437            "timestamp:1555779966\tfile:test"
438        );
439        let bin = b"test";
440        public_key
441            .verify(&bin[..], &signature, true)
442            .expect("Signature didn't verify");
443        let bin = b"Test";
444        match public_key.verify(&bin[..], &signature, true) {
445            Err(Error::InvalidSignature) => {}
446            _ => panic!("Invalid signature verified"),
447        };
448
449        let public_key2 = PublicKey::decode(
450            "untrusted comment: minisign public key E7620F1842B4E81F
451RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3",
452        )
453        .expect("Unable to decode the public key");
454        assert_eq!(
455            public_key2.untrusted_comment(),
456            Some("untrusted comment: minisign public key E7620F1842B4E81F")
457        );
458        match public_key2.verify(&bin[..], &signature, true) {
459            Err(Error::InvalidSignature) => {}
460            _ => panic!("Invalid signature verified"),
461        };
462    }
463
464    #[test]
465    fn verify_prehashed() {
466        let public_key =
467            PublicKey::from_base64("RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3")
468                .expect("Unable to decode the public key");
469        assert_eq!(public_key.untrusted_comment(), None);
470        let signature = Signature::decode(
471            "untrusted comment: signature from minisign secret key
472RUQf6LRCGA9i559r3g7V1qNyJDApGip8MfqcadIgT9CuhV3EMhHoN1mGTkUidF/\
473             z7SrlQgXdy8ofjb7bNJJylDOocrCo8KLzZwo=
474trusted comment: timestamp:1556193335\tfile:test
475y/rUw2y8/hOUYjZU71eHp/Wo1KZ40fGy2VJEDl34XMJM+TX48Ss/17u3IvIfbVR1FkZZSNCisQbuQY+bHwhEBg==",
476        )
477        .expect("Unable to decode the signature");
478        assert_eq!(
479            signature.untrusted_comment(),
480            "untrusted comment: signature from minisign secret key"
481        );
482        assert_eq!(
483            signature.trusted_comment(),
484            "timestamp:1556193335\tfile:test"
485        );
486        let bin = b"test";
487        public_key
488            .verify(&bin[..], &signature, false)
489            .expect("Signature didn't verify");
490    }
491
492    #[test]
493    fn verify_stream() {
494        let public_key =
495            PublicKey::from_base64("RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3")
496                .expect("Unable to decode the public key");
497        assert_eq!(public_key.untrusted_comment(), None);
498        let signature = Signature::decode(
499            "untrusted comment: signature from minisign secret key
500RUQf6LRCGA9i559r3g7V1qNyJDApGip8MfqcadIgT9CuhV3EMhHoN1mGTkUidF/\
501             z7SrlQgXdy8ofjb7bNJJylDOocrCo8KLzZwo=
502trusted comment: timestamp:1556193335\tfile:test
503y/rUw2y8/hOUYjZU71eHp/Wo1KZ40fGy2VJEDl34XMJM+TX48Ss/17u3IvIfbVR1FkZZSNCisQbuQY+bHwhEBg==",
504        )
505        .expect("Unable to decode the signature");
506        assert_eq!(
507            signature.untrusted_comment(),
508            "untrusted comment: signature from minisign secret key"
509        );
510        assert_eq!(
511            signature.trusted_comment(),
512            "timestamp:1556193335\tfile:test"
513        );
514        let mut stream_verifier = public_key
515            .verify_stream(&signature)
516            .expect("Can't extract StreamerVerifier");
517
518        let bin: &[u8] = b"te";
519        stream_verifier.update(bin);
520
521        let bin: &[u8] = b"st";
522        stream_verifier.update(bin);
523
524        stream_verifier.finalize().expect("Signature didn't verify");
525    }
526}