minisign_verify/
lib.rs

1//! A small crate to verify Minisign signatures.
2//!
3//! # Example
4//!
5//! ```rust
6//! use minisign_verify::{PublicKey, Signature};
7//!
8//! let public_key =
9//!     PublicKey::from_base64("RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3")
10//!         .expect("Unable to decode the public key");
11//!
12//! let signature = Signature::decode(
13//!     "untrusted comment: signature from minisign secret key
14//! RUQf6LRCGA9i559r3g7V1qNyJDApGip8MfqcadIgT9CuhV3EMhHoN1mGTkUidF/\
15//!      z7SrlQgXdy8ofjb7bNJJylDOocrCo8KLzZwo=
16//! trusted comment: timestamp:1633700835\tfile:test\tprehashed
17//! wLMDjy9FLAuxZ3q4NlEvkgtyhrr0gtTu6KC4KBJdITbbOeAi1zBIYo0v4iTgt8jJpIidRJnp94ABQkJAgAooBQ==",
18//! )
19//! .expect("Unable to decode the signature");
20//!
21//! let bin = b"test";
22//! public_key
23//!     .verify(&bin[..], &signature, false)
24//!     .expect("Signature didn't verify");
25//! ```
26
27mod base64;
28mod crypto;
29
30use std::path::Path;
31use std::{fmt, fs, io};
32
33use base64::{Base64, Decoder};
34
35use crate::crypto::blake2b::{Blake2b, BLAKE2B_OUTBYTES};
36use crate::crypto::ed25519;
37#[derive(Debug)]
38pub enum Error {
39    InvalidEncoding,
40    InvalidSignature,
41    IoError(io::Error),
42    UnexpectedAlgorithm,
43    UnexpectedKeyId,
44    UnsupportedAlgorithm,
45    UnsupportedLegacyMode,
46}
47
48impl fmt::Display for Error {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        write!(f, "{:?}", self)
51    }
52}
53
54impl std::error::Error for Error {
55    fn description(&self) -> &str {
56        match self {
57            Error::InvalidEncoding => "Invalid encoding",
58            Error::InvalidSignature => "Invalid signature",
59            Error::IoError(_) => "I/O error",
60            Error::UnexpectedAlgorithm => "Unexpected algorithm",
61            Error::UnexpectedKeyId => "Unexpected key identifier",
62            Error::UnsupportedAlgorithm => "Unsupported algorithm",
63            Error::UnsupportedLegacyMode => {
64                "Unsupported operration - StreamVerifier only supports non-legacy mode"
65            }
66        }
67    }
68
69    fn cause(&self) -> Option<&dyn std::error::Error> {
70        match self {
71            Error::IoError(e) => Some(e),
72            _ => None,
73        }
74    }
75}
76
77impl From<base64::Error> for Error {
78    fn from(_e: base64::Error) -> Error {
79        Error::InvalidEncoding
80    }
81}
82
83impl From<io::Error> for Error {
84    fn from(e: io::Error) -> Error {
85        Error::IoError(e)
86    }
87}
88
89/// A Minisign public key
90#[derive(Clone, Debug, Eq, PartialEq)]
91pub struct PublicKey {
92    untrusted_comment: Option<String>,
93    signature_algorithm: [u8; 2],
94    key_id: [u8; 8],
95    key: [u8; 32],
96}
97
98/// A StreamVerifier to verify a signature against a data stream
99/// NOTE: this mode of operation does not support the legacy signature model
100#[derive(Clone)]
101pub struct StreamVerifier<'a> {
102    public_key: &'a PublicKey,
103    signature: &'a Signature,
104    hasher: Blake2b,
105}
106
107/// A Minisign signature
108#[derive(Clone)]
109pub struct Signature {
110    untrusted_comment: String,
111    key_id: [u8; 8],
112    signature: [u8; 64],
113    trusted_comment: String,
114    global_signature: [u8; 64],
115    is_prehashed: bool,
116}
117
118impl Signature {
119    /// Create a Minisign signature from a string
120    pub fn decode(lines_str: &str) -> Result<Self, Error> {
121        let mut lines = lines_str.lines();
122        let untrusted_comment = lines.next().ok_or(Error::InvalidEncoding)?.to_string();
123        let bin1 = Base64::decode_to_vec(lines.next().ok_or(Error::InvalidEncoding)?)?;
124        if bin1.len() != 74 {
125            return Err(Error::InvalidEncoding);
126        }
127        let trusted_comment = lines.next().ok_or(Error::InvalidEncoding)?.to_string();
128        let bin2 = Base64::decode_to_vec(lines.next().ok_or(Error::InvalidEncoding)?)?;
129        if bin2.len() != 64 {
130            return Err(Error::InvalidEncoding);
131        }
132        if !trusted_comment.starts_with("trusted comment: ") {
133            return Err(Error::InvalidEncoding);
134        }
135        let mut signature_algorithm = [0u8; 2];
136        signature_algorithm.copy_from_slice(&bin1[0..2]);
137        let mut key_id = [0u8; 8];
138        key_id.copy_from_slice(&bin1[2..10]);
139        let mut signature = [0u8; 64];
140        signature.copy_from_slice(&bin1[10..74]);
141        let mut global_signature = [0u8; 64];
142        global_signature.copy_from_slice(&bin2);
143        let is_prehashed = match (signature_algorithm[0], signature_algorithm[1]) {
144            (0x45, 0x64) => false,
145            (0x45, 0x44) => true,
146            _ => return Err(Error::UnsupportedAlgorithm),
147        };
148        Ok(Signature {
149            untrusted_comment,
150            key_id,
151            signature,
152            trusted_comment,
153            global_signature,
154            is_prehashed,
155        })
156    }
157
158    /// Load a Minisign signature from a `.sig` file
159    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
160        let bin = fs::read_to_string(path)?;
161        Signature::decode(&bin)
162    }
163
164    /// Return the trusted comment of the signature
165    pub fn trusted_comment(&self) -> &str {
166        &self.trusted_comment[17..]
167    }
168
169    /// Return the untrusted comment of the signature
170    pub fn untrusted_comment(&self) -> &str {
171        &self.untrusted_comment
172    }
173}
174
175impl<'a> PublicKey {
176    /// Create a Minisign public key from a base64 string
177    pub fn from_base64(public_key_b64: &str) -> Result<Self, Error> {
178        let bin = Base64::decode_to_vec(public_key_b64)?;
179        if bin.len() != 42 {
180            return Err(Error::InvalidEncoding);
181        }
182        let mut signature_algorithm = [0u8; 2];
183        signature_algorithm.copy_from_slice(&bin[0..2]);
184        match (signature_algorithm[0], signature_algorithm[1]) {
185            (0x45, 0x64) | (0x45, 0x44) => {}
186            _ => return Err(Error::UnsupportedAlgorithm),
187        };
188        let mut key_id = [0u8; 8];
189        key_id.copy_from_slice(&bin[2..10]);
190        let mut key = [0u8; 32];
191        key.copy_from_slice(&bin[10..42]);
192        Ok(PublicKey {
193            untrusted_comment: None,
194            signature_algorithm,
195            key_id,
196            key,
197        })
198    }
199
200    /// Create a Minisign public key from a string, as in the `minisign.pub`
201    /// file
202    pub fn decode(lines_str: &str) -> Result<Self, Error> {
203        let mut lines = lines_str.lines();
204        let untrusted_comment = lines.next().ok_or(Error::InvalidEncoding)?;
205        let public_key_b64 = lines.next().ok_or(Error::InvalidEncoding)?;
206        let mut public_key = PublicKey::from_base64(public_key_b64)?;
207        public_key.untrusted_comment = Some(untrusted_comment.to_string());
208        Ok(public_key)
209    }
210
211    /// Load a Minisign key from a file (such as the `minisign.pub` file)
212    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
213        let bin = fs::read_to_string(path)?;
214        PublicKey::decode(&bin)
215    }
216
217    /// Return the untrusted comment, if there is one
218    pub fn untrusted_comment(&self) -> Option<&str> {
219        self.untrusted_comment.as_deref()
220    }
221
222    fn verify_ed25519(&self, bin: &[u8], signature: &Signature) -> Result<(), Error> {
223        if !ed25519::verify(bin, &self.key, &signature.signature) {
224            return Err(Error::InvalidSignature);
225        }
226        let trusted_comment_bin = signature.trusted_comment().as_bytes();
227        let mut global = Vec::with_capacity(signature.signature.len() + trusted_comment_bin.len());
228        global.extend_from_slice(&signature.signature[..]);
229        global.extend_from_slice(trusted_comment_bin);
230        if !ed25519::verify(&global, &self.key, &signature.global_signature) {
231            return Err(Error::InvalidSignature);
232        }
233        Ok(())
234    }
235
236    /// Verify that `signature` is a valid signature for `bin` using this public
237    /// key `allow_legacy` should only be set to `true` in order to support
238    /// signatures made by older versions of Minisign.
239    pub fn verify(
240        &self,
241        bin: &[u8],
242        signature: &Signature,
243        allow_legacy: bool,
244    ) -> Result<(), Error> {
245        if self.key_id != signature.key_id {
246            return Err(Error::UnexpectedKeyId);
247        }
248        let mut h;
249        let bin = if signature.is_prehashed {
250            h = vec![0u8; BLAKE2B_OUTBYTES];
251            Blake2b::blake2b(&mut h, bin);
252            &h
253        } else if !allow_legacy {
254            return Err(Error::UnexpectedAlgorithm);
255        } else {
256            bin
257        };
258        self.verify_ed25519(bin, signature)
259    }
260
261    /// Sets up a stream verifier that can be use iteratively.
262    pub fn verify_stream(&'a self, signature: &'a Signature) -> Result<StreamVerifier, Error> {
263        if self.key_id != signature.key_id {
264            return Err(Error::UnexpectedKeyId);
265        }
266        if !signature.is_prehashed {
267            return Err(Error::UnsupportedLegacyMode);
268        }
269        let hasher = Blake2b::new(BLAKE2B_OUTBYTES);
270        Ok(StreamVerifier {
271            public_key: self,
272            signature,
273            hasher,
274        })
275    }
276}
277
278impl<'a> StreamVerifier<'a> {
279    pub fn update(&mut self, buf: &[u8]) {
280        self.hasher.update(buf);
281    }
282
283    pub fn finalize(&mut self) -> Result<(), Error> {
284        let mut bin = vec![0u8; BLAKE2B_OUTBYTES];
285        self.hasher.finalize(&mut bin);
286        self.public_key.verify_ed25519(&bin, self.signature)
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293    #[test]
294    fn verify() {
295        let public_key =
296            PublicKey::from_base64("RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3")
297                .expect("Unable to decode the public key");
298        assert_eq!(public_key.untrusted_comment(), None);
299        let signature = Signature::decode(
300            "untrusted comment: signature from minisign secret key
301RWQf6LRCGA9i59SLOFxz6NxvASXDJeRtuZykwQepbDEGt87ig1BNpWaVWuNrm73YiIiJbq71Wi+dP9eKL8OC351vwIasSSbXxwA=
302trusted comment: timestamp:1555779966\tfile:test
303QtKMXWyYcwdpZAlPF7tE2ENJkRd1ujvKjlj1m9RtHTBnZPa5WKU5uWRs5GoP5M/VqE81QFuMKI5k/SfNQUaOAA==",
304        )
305        .expect("Unable to decode the signature");
306        assert_eq!(
307            signature.untrusted_comment(),
308            "untrusted comment: signature from minisign secret key"
309        );
310        assert_eq!(
311            signature.trusted_comment(),
312            "timestamp:1555779966\tfile:test"
313        );
314        let bin = b"test";
315        public_key
316            .verify(&bin[..], &signature, true)
317            .expect("Signature didn't verify");
318        let bin = b"Test";
319        match public_key.verify(&bin[..], &signature, true) {
320            Err(Error::InvalidSignature) => {}
321            _ => panic!("Invalid signature verified"),
322        };
323
324        let public_key2 = PublicKey::decode(
325            "untrusted comment: minisign public key E7620F1842B4E81F
326RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3",
327        )
328        .expect("Unable to decode the public key");
329        assert_eq!(
330            public_key2.untrusted_comment(),
331            Some("untrusted comment: minisign public key E7620F1842B4E81F")
332        );
333        match public_key2.verify(&bin[..], &signature, true) {
334            Err(Error::InvalidSignature) => {}
335            _ => panic!("Invalid signature verified"),
336        };
337    }
338
339    #[test]
340    fn verify_prehashed() {
341        let public_key =
342            PublicKey::from_base64("RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3")
343                .expect("Unable to decode the public key");
344        assert_eq!(public_key.untrusted_comment(), None);
345        let signature = Signature::decode(
346            "untrusted comment: signature from minisign secret key
347RUQf6LRCGA9i559r3g7V1qNyJDApGip8MfqcadIgT9CuhV3EMhHoN1mGTkUidF/\
348             z7SrlQgXdy8ofjb7bNJJylDOocrCo8KLzZwo=
349trusted comment: timestamp:1556193335\tfile:test
350y/rUw2y8/hOUYjZU71eHp/Wo1KZ40fGy2VJEDl34XMJM+TX48Ss/17u3IvIfbVR1FkZZSNCisQbuQY+bHwhEBg==",
351        )
352        .expect("Unable to decode the signature");
353        assert_eq!(
354            signature.untrusted_comment(),
355            "untrusted comment: signature from minisign secret key"
356        );
357        assert_eq!(
358            signature.trusted_comment(),
359            "timestamp:1556193335\tfile:test"
360        );
361        let bin = b"test";
362        public_key
363            .verify(&bin[..], &signature, false)
364            .expect("Signature didn't verify");
365    }
366
367    #[test]
368    fn verify_stream() {
369        let public_key =
370            PublicKey::from_base64("RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3")
371                .expect("Unable to decode the public key");
372        assert_eq!(public_key.untrusted_comment(), None);
373        let signature = Signature::decode(
374            "untrusted comment: signature from minisign secret key
375RUQf6LRCGA9i559r3g7V1qNyJDApGip8MfqcadIgT9CuhV3EMhHoN1mGTkUidF/\
376             z7SrlQgXdy8ofjb7bNJJylDOocrCo8KLzZwo=
377trusted comment: timestamp:1556193335\tfile:test
378y/rUw2y8/hOUYjZU71eHp/Wo1KZ40fGy2VJEDl34XMJM+TX48Ss/17u3IvIfbVR1FkZZSNCisQbuQY+bHwhEBg==",
379        )
380        .expect("Unable to decode the signature");
381        assert_eq!(
382            signature.untrusted_comment(),
383            "untrusted comment: signature from minisign secret key"
384        );
385        assert_eq!(
386            signature.trusted_comment(),
387            "timestamp:1556193335\tfile:test"
388        );
389        let mut stream_verifier = public_key
390            .verify_stream(&signature)
391            .expect("Can't extract StreamerVerifier");
392
393        let bin: &[u8] = b"te";
394        stream_verifier.update(bin);
395
396        let bin: &[u8] = b"st";
397        stream_verifier.update(bin);
398
399        stream_verifier.finalize().expect("Signature didn't verify");
400    }
401}