1use minisign_verify::{PublicKey, Signature};
4use sha2::{Digest, Sha256};
5
6use crate::installer::error::InstallerError;
7
8pub(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
16pub(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 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
55pub(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}