CH-J Toolsġestjoni tas-server fuq SSH
Menu
Sors ippubblikat

CH-J Tools

direttorji browse u fajls għal rilaxx applikazzjoni speċifika.

Sors ta' tniżżil ZIP
CH-J / Server Manager 1704 fajls
0.1.2-beta Miżjud: · Buillt:
quick.rs Miżjud: Data mhux irreġistrata 16.3 KB · 307 linji
Kodiċi Fajl tat-tniżżil
1//! Quick actions: verifying or signing one local file from the system file manager (Finder, Explorer,
2//! Nautilus/Dolphin) without the main window. The request arrives as command-line arguments
3//! `--chj-quick <verify|sign> <absolute path>`, either at start or forwarded by the single-instance plugin.
4//!
5//! Security: a request only opens the small quick window. Nothing is signed without the user choosing the
6//! key in that window and typing its passphrase; the window always shows the full path of the file.
7use crate::core::error::{CoreError, CoreResult};
8use parking_lot::Mutex;
9use serde_json::{json, Value};
10use std::path::{Path, PathBuf};
12pub const QUICK_FLAG: &str = "--chj-quick";
13/// Detached signature extensions, in the order a verification looks for them next to the file.
14pub const SIGNATURE_EXTENSIONS: &[&str] = &["asc", "sig", "gpg"];
15const MAX_SIGNATURE: u64 = 1024 * 1024;
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub enum QuickAction {
19 Verify,
20 Sign,
23impl QuickAction {
24 pub fn name(self) -> &'static str {
25 match self {
26 QuickAction::Verify => "verify",
27 QuickAction::Sign => "sign",
28 }
29 }
32#[derive(Clone, Debug, PartialEq, Eq)]
33pub struct QuickRequest {
34 pub action: QuickAction,
35 pub path: PathBuf,
38/// Finds `--chj-quick <action> <path>` anywhere in `args` (the program name may or may not be included).
39pub fn parse_args<S: AsRef<str>>(args: &[S]) -> Option<QuickRequest> {
40 let position = args.iter().position(|a| a.as_ref() == QUICK_FLAG)?;
41 let action = match args.get(position + 1)?.as_ref() {
42 "verify" => QuickAction::Verify,
43 "sign" => QuickAction::Sign,
44 _ => return None,
45 };
46 let path = PathBuf::from(args.get(position + 2)?.as_ref());
47 if !path.is_absolute() {
48 return None;
49 }
50 Some(QuickRequest { action, path })
53/// Pending and current requests of this process. Only one quick window exists at a time; a new request
54/// replaces the shown one (the window reloads its state).
55#[derive(Default)]
56pub struct QuickSlot {
57 current: Mutex<Option<QuickRequest>>,
58 /// The process was started only for a quick action; closing the quick window then ends it.
59 pub only_quick: Mutex<bool>,
62impl QuickSlot {
63 pub fn set(&self, request: QuickRequest) {
64 *self.current.lock() = Some(request);
65 }
66 pub fn get(&self) -> Option<QuickRequest> {
67 self.current.lock().clone()
68 }
69 pub fn clear(&self) {
70 *self.current.lock() = None;
71 }
74fn regular_file(path: &Path) -> CoreResult<std::fs::Metadata> {
75 let meta = std::fs::metadata(path).map_err(|_| CoreError::new("QUICK_FILE_MISSING", "The file does not exist or cannot be read."))?;
76 if !meta.is_file() {
77 return Err(CoreError::new("QUICK_NOT_A_FILE", "Only regular files can be signed or verified."));
78 }
79 Ok(meta)
82fn with_extension(path: &Path, ext: &str) -> PathBuf {
83 let mut name = path.as_os_str().to_os_string();
84 name.push(".");
85 name.push(ext);
86 PathBuf::from(name)
89/// The data file and its detached signature. The selected file may be either: `doc.pdf` (signature
90/// `doc.pdf.asc|.sig|.gpg` next to it) or the signature itself (`doc.pdf.asc` → data `doc.pdf`).
91pub fn locate_pair(selected: &Path) -> CoreResult<(PathBuf, Option<PathBuf>)> {
92 regular_file(selected)?;
93 let ext = selected.extension().and_then(|e| e.to_str()).map(str::to_ascii_lowercase);
94 if ext.is_some_and(|e| SIGNATURE_EXTENSIONS.contains(&e.as_str())) {
95 let data = selected.with_extension("");
96 if data != selected && data.is_file() {
97 return Ok((data, Some(selected.to_path_buf())));
98 }
99 }
100 let signature = SIGNATURE_EXTENSIONS.iter().map(|ext| with_extension(selected, ext)).find(|p| p.is_file());
101 Ok((selected.to_path_buf(), signature))
104/// Where a new signature is written: `<file>.asc` (ASCII armored, as GnuPG `--armor --detach-sign`).
105pub fn signature_target(file: &Path) -> PathBuf {
106 with_extension(file, "asc")
109pub fn file_summary(path: &Path) -> CoreResult<Value> {
110 let meta = regular_file(path)?;
111 Ok(json!({
112 "path": path.to_string_lossy(),
113 "name": path.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_default(),
114 "directory": path.parent().map(|p| p.to_string_lossy().to_string()).unwrap_or_default(),
115 "size": meta.len(),
116 }))
119pub fn read_signature(path: &Path) -> CoreResult<Vec<u8>> {
120 let meta = regular_file(path)?;
121 if meta.len() > MAX_SIGNATURE {
122 return Err(CoreError::new("QUICK_SIGNATURE_TOO_LARGE", "The signature file is too large."));
123 }
124 Ok(std::fs::read(path)?)
127/// Verifies `file` against `signature` with the stored Keychain identities first (when the vault is open),
128/// then with the public keys saved by the Hash & Checksum module. Returns the first valid match; when no key
129/// matches, the error of the saved-key path (with the issuer) explains what is missing.
130pub fn verify(file: &Path, signature: &[u8], keychain: &[Value], keyring: &Path) -> CoreResult<Value> {
131 for entry in keychain.iter().filter(|e| e["kind"] == "openpgp") {
132 if crate::core::keychain::verify(entry, file, signature).is_ok() {
133 let details = crate::core::keychain::describe(entry);
134 // Name the component that signed: an issuer that is this identity's primary key or one of its subkeys.
135 let issuers = crate::core::hashing::pgp::signature_issuers(signature);
136 let primary = entry["fingerprint"].as_str().unwrap_or("").to_ascii_uppercase();
137 let mut components = vec![(primary.clone(), details["keyId"].as_str().unwrap_or("").to_ascii_uppercase())];
138 components.extend(details["subkeys"].as_array().into_iter().flatten().map(|s| (s["fingerprint"].as_str().unwrap_or("").to_ascii_uppercase(), s["keyId"].as_str().unwrap_or("").to_ascii_uppercase())));
139 let signer = issuers
140 .iter()
141 .find_map(|issuer| components.iter().find(|(fp, id)| !issuer.is_empty() && (fp == issuer || id == issuer)).map(|(fp, _)| fp.clone()))
142 .unwrap_or(primary);
143 return Ok(json!({
144 "valid": true,
145 "keySource": "keychain",
146 "label": entry["label"],
147 "primaryFingerprint": entry["fingerprint"],
148 "signerFingerprint": signer,
149 "userIds": details["userIds"],
150 "revoked": details["revoked"],
151 "expired": details["expired"],
152 }));
153 }
154 }
155 // The signature names a Keychain identity (primary key or subkey) that did not verify it: the file or the
156 // signature changed after signing. Say so instead of asking for a key that is already there.
157 let issuers = crate::core::hashing::pgp::signature_issuers(signature);
158 let known = keychain.iter().filter(|e| e["kind"] == "openpgp").any(|entry| {
159 let details = crate::core::keychain::describe(entry);
160 let mut ids = vec![entry["fingerprint"].as_str().unwrap_or("").to_ascii_uppercase(), details["keyId"].as_str().unwrap_or("").to_ascii_uppercase()];
161 for sub in details["subkeys"].as_array().into_iter().flatten() {
162 ids.push(sub["fingerprint"].as_str().unwrap_or("").to_ascii_uppercase());
163 ids.push(sub["keyId"].as_str().unwrap_or("").to_ascii_uppercase());
164 }
165 issuers.iter().any(|issuer| !issuer.is_empty() && ids.contains(issuer))
166 });
167 if known {
168 return Err(CoreError::new("QUICK_SIGNATURE_INVALID", "The signature does not match this file: the file or the signature was changed after signing."));
169 }
170 let mut result = crate::core::hashing::pgp::verify_file(file, signature, None, keyring, "", false)?;
171 result["keySource"] = json!("saved");
172 Ok(result)
175/// Largest public key file accepted for a one-off verification.
176pub const MAX_PUBLIC_KEY: u64 = 1024 * 1024;
178/// Normalizes a fingerprint typed by the user (spaces, lower case); empty input means "no expectation".
179pub fn expected_fingerprint(input: Option<&str>) -> CoreResult<Option<String>> {
180 let text: String = input.unwrap_or("").chars().filter(|c| !c.is_whitespace()).collect::<String>().to_ascii_uppercase();
181 if text.is_empty() {
182 return Ok(None);
183 }
184 if ![40, 64].contains(&text.len()) || !text.chars().all(|c| c.is_ascii_hexdigit()) {
185 return Err(CoreError::new("QUICK_FINGERPRINT_INVALID", "Enter the full fingerprint (40 or 64 hexadecimal characters)."));
186 }
187 Ok(Some(text))
190/// Verifies with a public key given for this verification only (picked file or keys.ch-j.de; nothing is stored).
191/// With `expected`, the key that verified must be that fingerprint (primary key or the signing subkey).
192pub fn verify_with_key(file: &Path, signature: &[u8], key: &[u8], source: &str, expected: Option<&str>) -> CoreResult<Value> {
193 let empty = std::env::temp_dir().join("chj-no-saved-keys");
194 let mut result = crate::core::hashing::pgp::verify_file(file, signature, Some(key), &empty, expected.unwrap_or(""), false)?;
195 result["keySource"] = json!(source);
196 result["expectedMatched"] = json!(expected.is_some());
197 Ok(result)
200/// Fingerprints (or key IDs) the signature names as its issuer, for showing which key is needed.
201pub fn signature_issuers(signature: &[u8]) -> Vec<String> {
202 crate::core::hashing::pgp::signature_issuers(signature)
205#[cfg(test)]
206mod tests {
207 use super::*;
209 #[test]
210 fn parses_only_complete_absolute_requests() {
211 let abs = if cfg!(windows) { "C:\\data\\doc.pdf" } else { "/data/doc.pdf" };
212 let request = parse_args(&["app", QUICK_FLAG, "sign", abs]).unwrap();
213 assert_eq!(request.action, QuickAction::Sign);
214 assert_eq!(request.path, PathBuf::from(abs));
215 assert_eq!(parse_args(&[QUICK_FLAG, "verify", abs]).unwrap().action, QuickAction::Verify);
216 assert!(parse_args(&["app", QUICK_FLAG, "sign", "relative.pdf"]).is_none());
217 assert!(parse_args(&["app", QUICK_FLAG, "delete", abs]).is_none());
218 assert!(parse_args(&["app", QUICK_FLAG, "sign"]).is_none());
219 assert!(parse_args(&["app", abs]).is_none());
220 }
222 #[test]
223 fn finds_signature_from_either_side() {
224 let dir = tempfile::tempdir().unwrap();
225 let data = dir.path().join("doc.pdf");
226 std::fs::write(&data, b"data").unwrap();
227 assert_eq!(locate_pair(&data).unwrap(), (data.clone(), None));
228 let sig = dir.path().join("doc.pdf.sig");
229 std::fs::write(&sig, b"sig").unwrap();
230 assert_eq!(locate_pair(&data).unwrap(), (data.clone(), Some(sig.clone())));
231 let asc = dir.path().join("doc.pdf.asc");
232 std::fs::write(&asc, b"asc").unwrap();
233 assert_eq!(locate_pair(&data).unwrap().1, Some(asc.clone()), ".asc wins over .sig");
234 assert_eq!(locate_pair(&sig).unwrap(), (data.clone(), Some(sig.clone())), "signature selected");
235 assert_eq!(signature_target(&data), asc);
236 assert!(locate_pair(dir.path()).is_err(), "directory");
237 assert!(locate_pair(&dir.path().join("missing")).is_err());
238 }
240 #[test]
241 fn verifies_with_a_picked_public_key_and_names_wrong_keys_and_changed_files() {
242 use crate::core::keychain;
243 let dir = tempfile::tempdir().unwrap();
244 let file = dir.path().join("payload.bin");
245 std::fs::write(&file, b"payload checked with an external public key").unwrap();
246 let (_, entry) = crate::core::openpgp::generate(&json!({"name":"External signer","version":4,"primaryCapabilities":"CS","passphrase":"fixture-password"})).unwrap();
247 let (_, other) = crate::core::openpgp::generate(&json!({"name":"Somebody else","version":4,"primaryCapabilities":"CS","passphrase":"fixture-password"})).unwrap();
248 let armored = keychain::sign_with(&entry, &file, "fixture-password", None).unwrap();
249 let public = entry["public"].as_str().unwrap().as_bytes().to_vec();
250 let result = verify_with_key(&file, armored.as_bytes(), &public, "file", None).unwrap();
251 assert_eq!(result["keySource"], "file");
252 assert_eq!(result["valid"], true);
253 let wrong = verify_with_key(&file, armored.as_bytes(), other["public"].as_str().unwrap().as_bytes(), "file", None).unwrap_err();
254 assert_eq!(wrong.code, "HASH_PGP_SIGNER_UNKNOWN");
255 assert_eq!(verify(&file, armored.as_bytes(), &[], &dir.path().join("keyring")).unwrap_err().code, "HASH_PGP_KEY_REQUIRED", "unknown key: ask for it");
256 let primary = entry["fingerprint"].as_str().unwrap().to_string();
257 let spaced: String = primary.to_ascii_lowercase().chars().collect::<Vec<_>>().chunks(4).map(|c| c.iter().collect::<String>()).collect::<Vec<_>>().join(" ");
258 let expected = expected_fingerprint(Some(&spaced)).unwrap().unwrap();
259 assert_eq!(verify_with_key(&file, armored.as_bytes(), &public, "keyserver", Some(&expected)).unwrap()["expectedMatched"], true);
260 let other_fp = expected_fingerprint(other["fingerprint"].as_str()).unwrap().unwrap();
261 assert_eq!(verify_with_key(&file, armored.as_bytes(), &public, "file", Some(&other_fp)).unwrap_err().code, "HASH_PGP_FINGERPRINT_MISMATCH");
262 assert!(expected_fingerprint(Some("ABCD")).is_err());
263 assert_eq!(expected_fingerprint(Some(" ")).unwrap(), None);
264 assert_eq!(signature_issuers(armored.as_bytes()).first().map(String::as_str), Some(primary.as_str()));
265 std::fs::write(&file, b"changed").unwrap();
266 assert!(verify_with_key(&file, armored.as_bytes(), &public, "file", None).is_err(), "changed file with the right key");
267 }
269 #[test]
270 fn signature_selected_without_its_data_is_treated_as_data() {
271 let dir = tempfile::tempdir().unwrap();
272 let lone = dir.path().join("orphan.asc");
273 std::fs::write(&lone, b"x").unwrap();
274 assert_eq!(locate_pair(&lone).unwrap(), (lone.clone(), None));
275 }
277 #[test]
278 fn signs_with_the_chosen_primary_or_subkey_and_verifies_from_the_keychain() {
279 use crate::core::keychain;
280 use pgp::composed::Deserializable;
281 let dir = tempfile::tempdir().unwrap();
282 let file = dir.path().join("payload.bin");
283 std::fs::write(&file, b"synthetic quick-action payload").unwrap();
284 let (_, entry) = crate::core::openpgp::generate(&json!({"name":"Quick fixture","version":6,"primaryCapabilities":"CS","passphrase":"fixture-password","subkeys":[{"role":"sign","algorithm":"ed25519"},{"role":"sign","algorithm":"ed25519"},{"role":"encrypt","algorithm":"curve25519"}]})).unwrap();
285 let components = keychain::signing_components(&entry).unwrap();
286 assert_eq!(components.len(), 3, "primary and two signing subkeys, no encryption subkey: {components:?}");
287 assert_eq!(components[0]["role"], "primary");
288 let encrypt = keychain::describe(&entry)["subkeys"].as_array().unwrap().iter().find(|s| s["effectiveCapabilities"] == "E").unwrap()["fingerprint"].as_str().unwrap().to_string();
289 assert!(keychain::sign_with(&entry, &file, "fixture-password", Some(&encrypt)).is_err(), "encryption subkey must not sign");
290 for component in &components {
291 let fp = component["fingerprint"].as_str().unwrap();
292 assert!(keychain::sign_with(&entry, &file, "wrong", Some(fp)).is_err());
293 let armored = keychain::sign_with(&entry, &file, "fixture-password", Some(&fp.to_ascii_lowercase())).unwrap();
294 let (signature, _) = pgp::composed::DetachedSignature::from_armor_single(std::io::Cursor::new(armored.as_bytes())).unwrap();
295 let issuers: Vec<String> = signature.signature.config().map(|c| c.issuer_fingerprint()).unwrap_or_default().iter().map(|f| hex::encode_upper(f.as_ref())).collect();
296 assert_eq!(issuers, vec![fp.to_string()], "signature made by the chosen component");
297 let result = verify(&file, armored.as_bytes(), &[entry.clone()], &dir.path().join("keyring")).unwrap();
298 assert_eq!(result["keySource"], "keychain");
299 assert_eq!(result["primaryFingerprint"], entry["fingerprint"]);
300 assert_eq!(result["signerFingerprint"], json!(fp), "reported signer is the chosen component");
301 assert!(verify(&file, armored.as_bytes(), &[], &dir.path().join("keyring")).is_err(), "unknown key without the Keychain");
302 std::fs::write(&file, b"changed").unwrap();
303 assert_eq!(verify(&file, armored.as_bytes(), &[entry.clone()], &dir.path().join("keyring")).unwrap_err().code, "QUICK_SIGNATURE_INVALID", "changed file, known key");
304 std::fs::write(&file, b"synthetic quick-action payload").unwrap();
305 }
306 }
SHA-2563031cb1af0133810ebe472e867384df7a7a44186a3891208df0a198c183121b9

CH-J Proprietary Software License 1.14 · Sors huwa pprovdut taħt iż-CH-J Proprietary Software License 1.14. Id-disponibbiltà tagħha ma tibdilx it-termini tal-liċenzja jew tagħti drittijiet addizzjonali.

Archive SHA-256ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f
Fajls