Skip to main content

mini_sign/
signature.rs

1use std::fmt::Display;
2use std::vec;
3
4use base64::Engine;
5use ed25519_dalek::ed25519;
6
7use crate::errors::Result;
8use crate::util::validate_comment;
9use crate::{ErrorKind, SError, ALG_SIZE, KID_SIZE, SIGALG_PREHASHED, SIG_SIZE};
10/// A `SignatureBox` represents a minisign signature.
11///
12/// also can be output to a string and parse from a str.
13///
14/// # Security
15///
16/// This does not mean trusted_comment is verified.
17/// must verify the signature by `PublicKeyBox`.
18#[derive(Debug, Clone)]
19pub struct SignatureBox<'s> {
20    pub(crate) untrusted_comment: Option<&'s str>,
21    pub(crate) trusted_comment: Option<&'s str>,
22    pub(crate) signature: Signature,
23}
24fn parse_signature(s: &'_ str) -> Result<SignatureBox<'_>> {
25    let mut lines = s.lines();
26    let untrusted_comment = lines
27        .next()
28        .ok_or_else(|| {
29            SError::new(
30                crate::ErrorKind::SignatureError,
31                "missing untrusted comment",
32            )
33        })?
34        .strip_prefix("untrusted comment: ")
35        .ok_or_else(|| {
36            SError::new(
37                crate::ErrorKind::SignatureError,
38                "missing untrusted comment",
39            )
40        })?;
41    validate_comment(Some(untrusted_comment), ErrorKind::SignatureError)?;
42    let sig = lines
43        .next()
44        .ok_or_else(|| SError::new(crate::ErrorKind::SignatureError, "missing signature"))?;
45    let decoder = base64::engine::general_purpose::STANDARD;
46    let sig_format = decoder
47        .decode(sig.as_bytes())
48        .map_err(|e| SError::new(crate::ErrorKind::SignatureError, e))?;
49    if sig_format.len() != ALG_SIZE + KID_SIZE + SIG_SIZE {
50        return Err(SError::new(
51            crate::ErrorKind::SignatureError,
52            "invalid signature length",
53        ));
54    }
55    let sig_alg = &sig_format[..ALG_SIZE];
56    let key_id = &sig_format[ALG_SIZE..ALG_SIZE + KID_SIZE];
57    let sig = &sig_format[ALG_SIZE + KID_SIZE..];
58    let trusted_comment = lines
59        .next()
60        .ok_or_else(|| SError::new(crate::ErrorKind::SignatureError, "missing trusted comment"))?
61        .strip_prefix("trusted comment: ")
62        .ok_or_else(|| SError::new(crate::ErrorKind::SignatureError, "missing trusted comment"))?;
63    validate_comment(Some(trusted_comment), ErrorKind::SignatureError)?;
64    let global_sig = lines
65        .next()
66        .ok_or_else(|| SError::new(crate::ErrorKind::SignatureError, "missing global signature"))?;
67    if lines.next().is_some() {
68        return Err(SError::new(
69            crate::ErrorKind::SignatureError,
70            "unexpected extra data",
71        ));
72    }
73    let global_sig_format = decoder
74        .decode(global_sig.as_bytes())
75        .map_err(|e| SError::new(crate::ErrorKind::SignatureError, e))?;
76    if global_sig_format.len() != 64 {
77        return Err(SError::new(
78            crate::ErrorKind::SignatureError,
79            "invalid global signature length",
80        ));
81    }
82    SignatureBox::new(
83        Some(untrusted_comment),
84        Some(trusted_comment),
85        Signature::new(
86            sig_alg.try_into().unwrap(),
87            key_id.try_into().unwrap(),
88            sig.try_into().unwrap(),
89            ed25519::Signature::from_bytes(&global_sig_format.try_into().unwrap()),
90        ),
91    )
92}
93
94#[cfg(test)]
95#[test]
96fn test_parse_signature() {
97    use crate::{sign, KeyPairBox};
98
99    let password = b"password";
100    let k = KeyPairBox::generate(Some(password), None, None).unwrap();
101    let file = sign(
102        Some(&k.public_key_box),
103        &k.secret_key_box,
104        Some(password),
105        "test".as_bytes(),
106        Some("trusted comment"),
107        Some("untrusted comment"),
108    )
109    .unwrap()
110    .to_string();
111    let sig = parse_signature(&file).unwrap();
112    assert_eq!(file, sig.to_string());
113}
114#[cfg(test)]
115#[test]
116fn test_parse_signature_rejects_extra_lines() {
117    use crate::{sign, KeyPairBox};
118
119    let password = b"password";
120    let keypair = KeyPairBox::generate(Some(password), None, None).unwrap();
121    let file = format!(
122        "{}extra\n",
123        sign(
124            Some(&keypair.public_key_box),
125            &keypair.secret_key_box,
126            Some(password),
127            "test".as_bytes(),
128            Some("trusted comment"),
129            Some("untrusted comment"),
130        )
131        .unwrap()
132    );
133
134    assert!(parse_signature(&file).is_err());
135}
136#[cfg(test)]
137#[test]
138fn test_parse_signature_rejects_control_char_comments() {
139    use crate::{sign, KeyPairBox};
140
141    let password = b"password";
142    let keypair = KeyPairBox::generate(Some(password), None, None).unwrap();
143    let file = sign(
144        Some(&keypair.public_key_box),
145        &keypair.secret_key_box,
146        Some(password),
147        "test".as_bytes(),
148        Some("trusted comment"),
149        Some("untrusted comment"),
150    )
151    .unwrap()
152    .to_string()
153    .replace(
154        "trusted comment: trusted comment",
155        "trusted comment: trusted\0comment",
156    );
157
158    assert!(parse_signature(&file).is_err());
159}
160impl Display for SignatureBox<'_> {
161    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162        let mut s = String::new();
163        s.push_str("untrusted comment: ");
164        if let Some(c) = self.untrusted_comment {
165            s.push_str(c);
166        }
167        s.push('\n');
168        let encoder = base64::engine::general_purpose::STANDARD;
169        let mut sig_format = vec![];
170        sig_format.extend_from_slice(&self.signature.sig_alg);
171        sig_format.extend_from_slice(&self.signature.key_id);
172        sig_format.extend_from_slice(&self.signature.sig.to_bytes());
173        let sig = encoder.encode(&sig_format);
174        s.push_str(&sig);
175        s.push('\n');
176        s.push_str("trusted comment: ");
177        if let Some(c) = self.trusted_comment {
178            s.push_str(c);
179        }
180        s.push('\n');
181        let global_sig = encoder.encode(self.signature.global_sig.to_bytes());
182        s.push_str(&global_sig);
183        s.push('\n');
184        write!(f, "{}", s)
185    }
186}
187
188impl<'s> SignatureBox<'s> {
189    pub(crate) fn new(
190        untrusted_comment: Option<&'s str>,
191        trusted_comment: Option<&'s str>,
192        signature: Signature,
193    ) -> Result<Self> {
194        validate_comment(untrusted_comment, ErrorKind::SignatureError)?;
195        validate_comment(trusted_comment, ErrorKind::SignatureError)?;
196        Ok(Self {
197            untrusted_comment,
198            trusted_comment,
199            signature,
200        })
201    }
202    pub fn is_prehashed(&self) -> bool {
203        self.signature.sig_alg == SIGALG_PREHASHED
204    }
205    pub fn untrusted_comment(&self) -> Option<&'s str> {
206        self.untrusted_comment
207    }
208    pub fn trusted_comment(&self) -> Option<&'s str> {
209        self.trusted_comment
210    }
211    pub fn key_id(&self) -> &[u8; KID_SIZE] {
212        &self.signature.key_id
213    }
214    #[allow(clippy::should_implement_trait)]
215    pub fn from_str(s: &str) -> Result<SignatureBox<'_>> {
216        parse_signature(s)
217    }
218}
219#[derive(Debug, Clone)]
220pub(crate) struct Signature {
221    pub sig_alg: [u8; ALG_SIZE],
222    pub key_id: [u8; KID_SIZE],
223    pub sig: ed25519::Signature,
224    pub global_sig: ed25519::Signature,
225}
226impl Signature {
227    pub fn new(
228        sig_alg: [u8; ALG_SIZE],
229        key_id: [u8; KID_SIZE],
230        sig: ed25519::Signature,
231        global_sig: ed25519::Signature,
232    ) -> Self {
233        Self {
234            sig_alg,
235            key_id,
236            sig,
237            global_sig,
238        }
239    }
240}