CH-J Toolsquản lý máy chủ qua SSH
Menu
Mã nguồn đã công bố

CH-J Tools

Duyệt thư mục và tệp của một bản phát hành ứng dụng cụ thể.

Tải mã nguồn dạng ZIP
CH-J / Server Manager 1704 tệp
0.1.2-beta Ngày thêm: · Ngày dựng:
pgp.rs Ngày thêm: Chưa ghi nhận ngày 16,3 KB · 324 dòng
Mã Tải tệp xuống
1//! Verifies a local file against a detached OpenPGP signature with a key the user chose. Nothing is fetched from
2//! the network. A fingerprint alone cannot verify a signature, so it either checks the supplied key or selects a
3//! key saved earlier. A valid result proves who signed with that key, not that the key can be trusted.
5use crate::core::error::{CoreError, CoreResult};
6use crate::core::updates::openpgp::{fingerprint_hex, key_unexpired, normalize_fingerprint, signature_current, system_time};
7use pgp::composed::{Deserializable, DetachedSignature, SignedPublicKey};
8use pgp::packet::{Signature, SignatureType};
9use pgp::types::KeyDetails;
10use serde_json::{json, Value};
11use std::io::Cursor;
12use std::path::Path;
13use std::time::SystemTime;
15pub const MAX_KEY_BYTES: u64 = 2 * 1024 * 1024;
16pub const MAX_SIGNATURE_BYTES: u64 = 1024 * 1024;
18fn fail(code: &str, message: &str) -> CoreError {
19 CoreError::new(code, message)
22fn armored(bytes: &[u8]) -> bool {
23 String::from_utf8_lossy(&bytes[..bytes.len().min(4096)]).contains("-----BEGIN PGP")
26fn parse_signature(bytes: &[u8]) -> CoreResult<Signature> {
27 let malformed = || fail("HASH_PGP_SIGNATURE_MALFORMED", "The file is not a detached OpenPGP signature.");
28 let detached = if armored(bytes) {
29 DetachedSignature::from_armor_single(Cursor::new(bytes)).map_err(|_| malformed())?.0
30 } else {
31 DetachedSignature::from_bytes(Cursor::new(bytes)).map_err(|_| malformed())?
32 };
33 if !covers_document(detached.signature.typ()) {
34 return Err(malformed());
35 }
36 Ok(detached.signature)
39/// Only binary and text signatures are made over a document. Standalone and timestamp signatures hash no file data,
40/// so one of them would "verify" any file.
41fn covers_document(kind: Option<SignatureType>) -> bool {
42 matches!(kind, Some(SignatureType::Binary | SignatureType::Text))
45/// A public key file may hold several keys, armored or binary.
46pub fn parse_keys(bytes: &[u8]) -> CoreResult<Vec<SignedPublicKey>> {
47 let malformed = || fail("HASH_PGP_KEY_MALFORMED", "The file does not contain an OpenPGP public key.");
48 let keys: Vec<SignedPublicKey> = if armored(bytes) {
49 SignedPublicKey::from_armor_many(Cursor::new(bytes)).map_err(|_| malformed())?.0.filter_map(Result::ok).collect()
50 } else {
51 SignedPublicKey::from_bytes_many(Cursor::new(bytes)).map_err(|_| malformed())?.filter_map(Result::ok).collect()
52 };
53 if keys.is_empty() { Err(malformed()) } else { Ok(keys) }
56fn user_ids(key: &SignedPublicKey) -> Vec<String> {
57 key.details.users.iter().map(|user| String::from_utf8_lossy(user.id.id()).to_string()).collect()
60fn describe(key: &SignedPublicKey) -> Value {
61 json!({ "fingerprint": fingerprint_hex(&key.primary_key.fingerprint()), "userIds": user_ids(key) })
64/// Issuer fingerprints of a detached signature, then its issuer key IDs (informational; the signature still has to verify).
65pub fn signature_issuers(bytes: &[u8]) -> Vec<String> {
66 parse_signature(bytes).map(|signature| { let (fingerprints, ids) = issuers(&signature); fingerprints.into_iter().chain(ids).collect() }).unwrap_or_default()
69fn issuers(signature: &Signature) -> (Vec<String>, Vec<String>) {
70 (
71 signature.issuer_fingerprint().iter().map(|fp| fingerprint_hex(fp)).collect(),
72 signature.issuer_key_id().iter().map(|id| hex::encode_upper(id.as_ref())).collect(),
73 )
76/// Revoked or expired keys do not verify anything, even when the mathematics of the signature hold.
77fn primary_usable(key: &SignedPublicKey, now: SystemTime) -> bool {
78 if key.verify_bindings().is_err() || key.details.revocation_signatures.iter().any(|revocation| revocation.verify_key(&key.primary_key).is_ok()) {
79 return false;
80 }
81 let created = system_time(key.primary_key.created_at());
82 key.details.users.iter().flat_map(|user| user.signatures.iter()).filter(|sig| signature_current(sig, now)).max_by_key(|sig| sig.created().map(system_time)).is_some_and(|sig| key_unexpired(created, sig, now))
85fn subkey_usable(key: &SignedPublicKey, index: usize, now: SystemTime) -> bool {
86 let subkey = &key.public_subkeys[index];
87 let mut binding: Option<&Signature> = None;
88 for candidate in &subkey.signatures {
89 if candidate.verify_subkey_binding(&key.primary_key, &subkey.key).is_err() {
90 continue;
91 }
92 match candidate.typ() {
93 Some(SignatureType::SubkeyRevocation) => return false,
94 Some(SignatureType::SubkeyBinding) if signature_current(candidate, now) && binding.is_none_or(|b| b.created().map(system_time) < candidate.created().map(system_time)) => binding = Some(candidate),
95 _ => {}
96 }
97 }
98 binding.is_some_and(|b| key_unexpired(system_time(subkey.key.created_at()), b, now) && b.key_flags().sign())
101fn verify_with(key: &SignedPublicKey, signature: &Signature, file: &Path, now: SystemTime) -> Option<CoreResult<String>> {
102 let (fingerprints, ids) = issuers(signature);
103 let names = |fingerprint: String, id: String| fingerprints.contains(&fingerprint) || ids.iter().any(|value| value.eq_ignore_ascii_case(&id));
104 let open = || std::fs::File::open(file).map(|f| std::io::BufReader::with_capacity(1 << 20, f)).map_err(|_| fail("HASH_PGP_FILE_UNREADABLE", "The file could not be read."));
105 let invalid_key = || Err(fail("HASH_PGP_KEY_INVALID", "The key is revoked, expired, or has an invalid self-signature."));
106 let invalid = || fail("HASH_PGP_SIGNATURE_INVALID", "The signature does not match this file. The file was changed or the signature belongs to another file.");
107 if names(fingerprint_hex(&key.primary_key.fingerprint()), hex::encode_upper(key.primary_key.legacy_key_id().as_ref())) {
108 if !primary_usable(key, now) {
109 return Some(invalid_key());
110 }
111 return Some(open().and_then(|data| signature.verify(&key.primary_key, data).map_err(|_| invalid())).map(|_| fingerprint_hex(&key.primary_key.fingerprint())));
112 }
113 for (index, subkey) in key.public_subkeys.iter().enumerate() {
114 if names(fingerprint_hex(&subkey.key.fingerprint()), hex::encode_upper(subkey.key.legacy_key_id().as_ref())) {
115 if !primary_usable(key, now) || !subkey_usable(key, index, now) {
116 return Some(invalid_key());
117 }
118 return Some(open().and_then(|data| signature.verify(&subkey.key, data).map_err(|_| invalid())).map(|_| fingerprint_hex(&subkey.key.fingerprint())));
119 }
120 }
121 None
124fn saved_keys(keyring: &Path) -> Vec<SignedPublicKey> {
125 let Ok(entries) = std::fs::read_dir(keyring) else { return Vec::new() };
126 entries.flatten().filter_map(|entry| std::fs::read(entry.path()).ok()).filter_map(|bytes| parse_keys(&bytes).ok()).flatten().collect()
129pub fn list_keys(keyring: &Path) -> Value {
130 let mut keys: Vec<Value> = saved_keys(keyring).iter().map(describe).collect();
131 keys.sort_by(|a, b| a["fingerprint"].as_str().cmp(&b["fingerprint"].as_str()));
132 json!({ "keys": keys })
135pub fn remove_key(keyring: &Path, fingerprint: &str) -> CoreResult<Value> {
136 let fingerprint = expected_fingerprint(fingerprint)?.ok_or_else(|| fail("HASH_PGP_FINGERPRINT_INVALID", "Enter the full fingerprint of the key (40 or 64 hexadecimal characters)."))?;
137 match std::fs::remove_file(keyring.join(format!("{fingerprint}.asc"))) {
138 Ok(()) => Ok(json!({ "removed": true })),
139 Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(json!({ "removed": false })),
140 Err(_) => Err(fail("HASH_PGP_KEY_STORE_FAILED", "The saved key could not be removed.")),
141 }
144fn save_key(keyring: &Path, key: &SignedPublicKey) -> CoreResult<()> {
145 let failed = || fail("HASH_PGP_KEY_STORE_FAILED", "The key could not be saved.");
146 let armored = key.to_armored_bytes(Default::default()).map_err(|_| failed())?;
147 std::fs::create_dir_all(keyring).map_err(|_| failed())?;
148 std::fs::write(keyring.join(format!("{}.asc", fingerprint_hex(&key.primary_key.fingerprint()))), armored).map_err(|_| failed())
151/// Short key IDs can be forged, so only complete fingerprints count.
152fn expected_fingerprint(value: &str) -> CoreResult<Option<String>> {
153 let normalized = normalize_fingerprint(value);
154 if value.trim().is_empty() {
155 return Ok(None);
156 }
157 if ![40, 64].contains(&normalized.len()) || value.chars().any(|c| !c.is_ascii_hexdigit() && !c.is_whitespace() && c != ':') {
158 return Err(fail("HASH_PGP_FINGERPRINT_INVALID", "Enter the full fingerprint of the key (40 or 64 hexadecimal characters)."));
159 }
160 Ok(Some(normalized))
163/// `key` is the public key the user supplied now. Without it, a saved key is used, chosen by the signature's issuer.
164pub fn verify_file(file: &Path, signature: &[u8], key: Option<&[u8]>, keyring: &Path, fingerprint: &str, save: bool) -> CoreResult<Value> {
165 verify_at(file, signature, key, keyring, fingerprint, save, SystemTime::now())
168fn verify_at(file: &Path, signature: &[u8], key: Option<&[u8]>, keyring: &Path, fingerprint: &str, save: bool, now: SystemTime) -> CoreResult<Value> {
169 let expected = expected_fingerprint(fingerprint)?;
170 let signature = parse_signature(signature)?;
171 let supplied = key.is_some();
172 let keys = match key {
173 Some(bytes) => parse_keys(bytes)?,
174 None => saved_keys(keyring),
175 };
176 if !signature_current(&signature, now) {
177 return Err(fail("HASH_PGP_SIGNATURE_INVALID", "The signature has expired or is dated in the future."));
178 }
179 let mut outcome = None;
180 for key in &keys {
181 if let Some(result) = verify_with(key, &signature, file, now) {
182 outcome = Some((key, result));
183 break;
184 }
185 }
186 let Some((key, result)) = outcome else {
187 let (fingerprints, ids) = issuers(&signature);
188 let issuer = fingerprints.first().or(ids.first()).cloned().unwrap_or_default();
189 let (code, message) = if supplied {
190 ("HASH_PGP_SIGNER_UNKNOWN", "This signature was not made with the supplied key.")
191 } else {
192 ("HASH_PGP_KEY_REQUIRED", "A fingerprint alone cannot verify a signature. Add the signer's public key.")
193 };
194 return Err(fail(code, message).with("issuer", Value::from(issuer)));
195 };
196 let signer = result?;
197 let primary = fingerprint_hex(&key.primary_key.fingerprint());
198 if let Some(expected) = &expected {
199 if *expected != primary && *expected != signer {
200 return Err(fail("HASH_PGP_FINGERPRINT_MISMATCH", "The signature is valid, but it was made by a different key than the fingerprint you entered.").with("primaryFingerprint", Value::from(primary)));
201 }
202 }
203 if save && supplied {
204 save_key(keyring, key)?;
205 }
206 let created = signature.created().map(|time| chrono::DateTime::<chrono::Utc>::from(system_time(time)).to_rfc3339_opts(chrono::SecondsFormat::Secs, true));
207 Ok(json!({
208 "valid": true,
209 "primaryFingerprint": primary,
210 "signerFingerprint": signer,
211 "userIds": user_ids(key),
212 "createdAt": created,
213 "keySource": if supplied { "supplied" } else { "saved" },
214 "fingerprintMatched": expected.is_some(),
215 "saved": save && supplied
216 }))
219#[cfg(test)]
220mod tests {
221 use super::*;
222 use std::path::PathBuf;
224 // Fixtures come from real GnuPG, see tests/fixtures/pgp/README.md.
225 fn fixture(name: &str) -> PathBuf {
226 Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/pgp").join(name)
227 }
228 fn read(name: &str) -> Vec<u8> {
229 std::fs::read(fixture(name)).unwrap()
230 }
231 fn fingerprint() -> String {
232 String::from_utf8(read("signer.fingerprint")).unwrap().trim().to_string()
233 }
234 fn check(signature: &str, key: Option<&str>, keyring: &Path, expected: &str, save: bool) -> CoreResult<Value> {
235 verify_file(&fixture("data.txt"), &read(signature), key.map(read).as_deref(), keyring, expected, save)
236 }
238 #[test]
239 fn armored_and_binary_signatures_verify_with_a_public_key() {
240 let dir = tempfile::tempdir().unwrap();
241 for signature in ["data.txt.asc", "data.txt.sig"] {
242 for key in ["signer.asc", "signer.gpg"] {
243 let result = check(signature, Some(key), dir.path(), "", false).unwrap();
244 assert_eq!(result["valid"], true, "{signature} {key}");
245 assert_eq!(result["primaryFingerprint"], fingerprint());
246 assert_eq!(result["fingerprintMatched"], false);
247 assert!(result["userIds"][0].as_str().unwrap().contains("CH-J Test"));
248 }
249 }
250 }
252 #[test]
253 fn a_changed_file_fails() {
254 let dir = tempfile::tempdir().unwrap();
255 let changed = dir.path().join("changed.txt");
256 std::fs::write(&changed, "changed").unwrap();
257 let error = verify_file(&changed, &read("data.txt.asc"), Some(&read("signer.asc")), dir.path(), "", false).unwrap_err();
258 assert_eq!(error.code, "HASH_PGP_SIGNATURE_INVALID");
259 }
261 #[test]
262 fn another_key_is_not_the_signer() {
263 let dir = tempfile::tempdir().unwrap();
264 assert_eq!(check("data.txt.asc", Some("other.asc"), dir.path(), "", false).unwrap_err().code, "HASH_PGP_SIGNER_UNKNOWN");
265 }
267 #[test]
268 fn the_fingerprint_must_match_the_key() {
269 let dir = tempfile::tempdir().unwrap();
270 let spaced = fingerprint().as_bytes().chunks(4).map(|c| String::from_utf8_lossy(c).to_lowercase()).collect::<Vec<_>>().join(" ");
271 assert_eq!(check("data.txt.asc", Some("signer.asc"), dir.path(), &spaced, false).unwrap()["fingerprintMatched"], true);
272 let other = String::from_utf8(read("other.fingerprint")).unwrap();
273 assert_eq!(check("data.txt.asc", Some("signer.asc"), dir.path(), other.trim(), false).unwrap_err().code, "HASH_PGP_FINGERPRINT_MISMATCH");
274 assert_eq!(check("data.txt.asc", Some("signer.asc"), dir.path(), "ABCD1234", false).unwrap_err().code, "HASH_PGP_FINGERPRINT_INVALID");
275 }
277 #[test]
278 fn a_fingerprint_alone_needs_a_saved_key() {
279 let dir = tempfile::tempdir().unwrap();
280 let error = check("data.txt.asc", None, dir.path(), &fingerprint(), false).unwrap_err();
281 assert_eq!(error.code, "HASH_PGP_KEY_REQUIRED");
282 assert_eq!(check("data.txt.asc", Some("signer.asc"), dir.path(), "", true).unwrap()["saved"], true);
283 let result = check("data.txt.asc", None, dir.path(), &fingerprint(), false).unwrap();
284 assert_eq!(result["keySource"], "saved");
285 assert_eq!(result["fingerprintMatched"], true);
286 assert_eq!(list_keys(dir.path())["keys"].as_array().unwrap().len(), 1);
287 assert_eq!(remove_key(dir.path(), &fingerprint()).unwrap()["removed"], true);
288 assert_eq!(check("data.txt.asc", None, dir.path(), &fingerprint(), false).unwrap_err().code, "HASH_PGP_KEY_REQUIRED");
289 }
291 #[test]
292 fn a_saved_key_does_not_vouch_for_another_fingerprint() {
293 let dir = tempfile::tempdir().unwrap();
294 check("data.txt.asc", Some("signer.asc"), dir.path(), "", true).unwrap();
295 let other = String::from_utf8(read("other.fingerprint")).unwrap();
296 assert_eq!(check("data.txt.asc", None, dir.path(), other.trim(), false).unwrap_err().code, "HASH_PGP_FINGERPRINT_MISMATCH");
297 }
299 #[test]
300 fn only_document_signatures_count() {
301 assert!(covers_document(Some(SignatureType::Binary)));
302 assert!(covers_document(Some(SignatureType::Text)));
303 for kind in [SignatureType::Standalone, SignatureType::Timestamp, SignatureType::CertGeneric, SignatureType::SubkeyBinding] {
304 assert!(!covers_document(Some(kind)), "{kind:?}");
305 }
306 assert!(!covers_document(None));
307 }
309 #[test]
310 fn malformed_inputs_are_named() {
311 let dir = tempfile::tempdir().unwrap();
312 assert_eq!(verify_file(&fixture("data.txt"), b"nonsense", None, dir.path(), "", false).unwrap_err().code, "HASH_PGP_SIGNATURE_MALFORMED");
313 assert_eq!(verify_file(&fixture("data.txt"), &read("data.txt.asc"), Some(b"nonsense"), dir.path(), "", false).unwrap_err().code, "HASH_PGP_KEY_MALFORMED");
314 }
316 #[test]
317 fn revoked_and_expired_keys_do_not_verify() {
318 let dir = tempfile::tempdir().unwrap();
319 assert_eq!(check("data.txt.asc", Some("signer-revoked.asc"), dir.path(), "", false).unwrap_err().code, "HASH_PGP_KEY_INVALID");
320 let later = SystemTime::now() + std::time::Duration::from_secs(10 * 365 * 24 * 3600);
321 let error = verify_at(&fixture("data.txt"), &read("expiring.txt.asc"), Some(&read("expiring.asc")), dir.path(), "", false, later).unwrap_err();
322 assert_eq!(error.code, "HASH_PGP_KEY_INVALID");
323 }
SHA-2567b502417b6f373080f2f13d9d68f453becde89f76d0ced74ac82456caa25eca5

CH-J Proprietary Software License 1.14 · Mã nguồn được cung cấp theo CH-J Proprietary Software License 1.14. Việc công khai mã nguồn không thay đổi các điều khoản giấy phép và không cấp thêm quyền nào.

SHA-256 của tệp nénac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f
Tệp