Skip to main content

tempo_ext/installer/
verify.rs

1//! Minisign signature verification and SHA-256 checksums for release artifacts.
2
3use minisign_verify::{PublicKey, Signature};
4use sha2::{Digest, Sha256};
5
6use crate::installer::error::InstallerError;
7
8/// Decodes a base64-encoded minisign public key.
9pub(super) fn decode_public_key(encoded_key: &str) -> Result<PublicKey, InstallerError> {
10    PublicKey::from_base64(encoded_key).map_err(|err| InstallerError::SignatureFormat {
11        field: "release public key",
12        details: err.to_string(),
13    })
14}
15
16/// Verifies a minisign signature over `data` and checks that every entry in
17/// `expected_trusted_comments` appears in the signature's trusted comment
18/// (tab-separated tokens). This prevents cross-extension substitution and
19/// version replay attacks.
20pub(super) fn verify_signature(
21    artifact: &str,
22    data: &[u8],
23    encoded_signature: &str,
24    public_key: &PublicKey,
25    expected_trusted_comments: &[&str],
26) -> Result<(), InstallerError> {
27    let signature =
28        Signature::decode(encoded_signature).map_err(|err| InstallerError::SignatureFormat {
29            field: "release signature",
30            details: err.to_string(),
31        })?;
32
33    public_key
34        .verify(data, &signature, false)
35        .map_err(|_| InstallerError::SignatureVerificationFailed(artifact.to_string()))?;
36
37    // After cryptographic verification succeeds, the trusted comment is
38    // authenticated. Check that every expected token is present to
39    // prevent cross-extension substitution and version replay attacks.
40    let tc = signature.trusted_comment();
41    let tokens: Vec<&str> = tc.split('\t').collect();
42    for expected in expected_trusted_comments {
43        if !tokens.contains(expected) {
44            return Err(InstallerError::TrustedCommentMismatch {
45                artifact: artifact.to_string(),
46                expected: expected.to_string(),
47                actual: tc.to_string(),
48            });
49        }
50    }
51
52    Ok(())
53}
54
55/// Computes the SHA-256 digest of `data` and returns it as a lowercase hex string.
56pub(super) fn sha256_hex(data: &[u8]) -> String {
57    let mut hasher = Sha256::new();
58    hasher.update(data);
59    format!("{:x}", hasher.finalize())
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use minisign::KeyPair;
66    use std::io::Cursor;
67
68    fn test_keypair() -> (minisign::PublicKey, minisign::SecretKey) {
69        let KeyPair { pk, sk } = KeyPair::generate_unencrypted_keypair().unwrap();
70        (pk, sk)
71    }
72
73    #[test]
74    fn sha256_known_vector() {
75        assert_eq!(
76            sha256_hex(b"hello world"),
77            "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
78        );
79    }
80
81    #[test]
82    fn sha256_empty() {
83        assert_eq!(
84            sha256_hex(b""),
85            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
86        );
87    }
88
89    #[test]
90    fn decode_public_key_valid() {
91        let (pk, _) = test_keypair();
92        let encoded = pk.to_base64();
93        assert!(decode_public_key(&encoded).is_ok());
94    }
95
96    #[test]
97    fn decode_public_key_invalid() {
98        assert!(matches!(
99            decode_public_key("not-valid!!!"),
100            Err(InstallerError::SignatureFormat { .. })
101        ));
102    }
103
104    #[test]
105    fn verify_signature_valid() {
106        let (pk, sk) = test_keypair();
107        let data = b"test data";
108        let sig_box = minisign::sign(Some(&pk), &sk, Cursor::new(data), None, None).unwrap();
109        let sig_str = sig_box.into_string();
110
111        let verify_pk = decode_public_key(&pk.to_base64()).unwrap();
112        assert!(verify_signature("test", data, &sig_str, &verify_pk, &[]).is_ok());
113    }
114
115    #[test]
116    fn verify_signature_wrong_key() {
117        let (pk, sk) = test_keypair();
118        let (other_pk, _) = test_keypair();
119        let data = b"test data";
120        let sig_box = minisign::sign(Some(&pk), &sk, Cursor::new(data), None, None).unwrap();
121        let sig_str = sig_box.into_string();
122
123        let wrong_pk = decode_public_key(&other_pk.to_base64()).unwrap();
124        assert!(matches!(
125            verify_signature("test", data, &sig_str, &wrong_pk, &[]),
126            Err(InstallerError::SignatureVerificationFailed(_))
127        ));
128    }
129
130    #[test]
131    fn verify_signature_tampered_data() {
132        let (pk, sk) = test_keypair();
133        let data = b"original data";
134        let sig_box = minisign::sign(Some(&pk), &sk, Cursor::new(data), None, None).unwrap();
135        let sig_str = sig_box.into_string();
136
137        let verify_pk = decode_public_key(&pk.to_base64()).unwrap();
138        assert!(matches!(
139            verify_signature("test", b"tampered data", &sig_str, &verify_pk, &[]),
140            Err(InstallerError::SignatureVerificationFailed(_))
141        ));
142    }
143
144    #[test]
145    fn verify_signature_invalid_format() {
146        let (pk, _) = test_keypair();
147        let verify_pk = decode_public_key(&pk.to_base64()).unwrap();
148        assert!(matches!(
149            verify_signature("test", b"data", "not a valid signature", &verify_pk, &[]),
150            Err(InstallerError::SignatureFormat { .. })
151        ));
152    }
153
154    #[test]
155    fn verify_trusted_comment_match() {
156        let (pk, sk) = test_keypair();
157        let data = b"test data";
158        let sig_box = minisign::sign(
159            Some(&pk),
160            &sk,
161            Cursor::new(data),
162            Some("file:tempo-wallet-darwin-arm64\tversion:v1.0.0"),
163            None,
164        )
165        .unwrap();
166        let sig_str = sig_box.into_string();
167
168        let verify_pk = decode_public_key(&pk.to_base64()).unwrap();
169        assert!(
170            verify_signature(
171                "tempo-wallet",
172                data,
173                &sig_str,
174                &verify_pk,
175                &["file:tempo-wallet-darwin-arm64", "version:v1.0.0"],
176            )
177            .is_ok()
178        );
179    }
180
181    #[test]
182    fn verify_trusted_comment_mismatch() {
183        let (pk, sk) = test_keypair();
184        let data = b"test data";
185        let sig_box = minisign::sign(
186            Some(&pk),
187            &sk,
188            Cursor::new(data),
189            Some("file:tempo-mpp-darwin-arm64\tversion:v1.0.0"),
190            None,
191        )
192        .unwrap();
193        let sig_str = sig_box.into_string();
194
195        let verify_pk = decode_public_key(&pk.to_base64()).unwrap();
196        assert!(matches!(
197            verify_signature(
198                "tempo-wallet",
199                data,
200                &sig_str,
201                &verify_pk,
202                &["file:tempo-wallet-darwin-arm64", "version:v1.0.0"],
203            ),
204            Err(InstallerError::TrustedCommentMismatch { .. })
205        ));
206    }
207
208    #[test]
209    fn verify_trusted_comment_version_mismatch() {
210        let (pk, sk) = test_keypair();
211        let data = b"test data";
212        let sig_box = minisign::sign(
213            Some(&pk),
214            &sk,
215            Cursor::new(data),
216            Some("file:tempo-wallet-darwin-arm64\tversion:v1.0.0"),
217            None,
218        )
219        .unwrap();
220        let sig_str = sig_box.into_string();
221
222        let verify_pk = decode_public_key(&pk.to_base64()).unwrap();
223        assert!(matches!(
224            verify_signature(
225                "tempo-wallet",
226                data,
227                &sig_str,
228                &verify_pk,
229                &["file:tempo-wallet-darwin-arm64", "version:v2.0.0"],
230            ),
231            Err(InstallerError::TrustedCommentMismatch { .. })
232        ));
233    }
234
235    #[test]
236    fn verify_trusted_comment_empty_skips_check() {
237        let (pk, sk) = test_keypair();
238        let data = b"test data";
239        let sig_box = minisign::sign(
240            Some(&pk),
241            &sk,
242            Cursor::new(data),
243            Some("file:anything"),
244            None,
245        )
246        .unwrap();
247        let sig_str = sig_box.into_string();
248
249        let verify_pk = decode_public_key(&pk.to_base64()).unwrap();
250        assert!(verify_signature("test", data, &sig_str, &verify_pk, &[]).is_ok());
251    }
252}