CH-J Toolsgestione server su SSH
Menu
Sorgente pubblicata

CH-J Tools

Sfoglia directory e file per una specifica release di applicazioni.

Scarica sorgente ZIP
CH-J / Server Manager 1704 file
0.1.2-beta Aggiunto: · Costruito:
mod.rs Aggiunto: Data non registrata 17,5 KB · 350 righe
Codice Scarica file
1//! Compatibility profiles are isolated from the RFC 9580 backend and update trust.
2//! RFC 1991/2440 historical keys and LibrePGP draft-koch-librepgp-06.
3mod certificate;
4mod historical;
5mod key;
6mod librepgp;
7mod operations;
8mod protect;
9mod signature;
10mod wire;
11pub use operations::{edit, revoke, sign, verify};
13use crate::core::error::CoreResult;
14use serde_json::{json, Value};
16/// Inspect packets before passing input to rPGP, whose V5 implementation is incomplete.
17/// Mixed modern/compatibility certificates are rejected instead of falling through.
18pub fn handles(bytes: &[u8]) -> CoreResult<bool> {
19 let text = bytes.strip_prefix(b"\xef\xbb\xbf").unwrap_or(bytes);
20 if !text.starts_with(b"-----BEGIN PGP ") && !text.first().is_some_and(|b| b & 128 != 0) {
21 return Ok(false);
22 }
23 let packets = wire::packets(text)?;
24 let mut family = None;
25 for packet in packets.iter().filter(|p| [5, 6, 7, 14].contains(&p.tag)) {
26 let version = *packet.body.first().ok_or_else(wire::invalid)?;
27 let algorithm=packet.body.get(if version<=3{7}else{5}).copied().ok_or_else(wire::invalid)?;
28 if (30..=36).contains(&algorithm) && !(version==6 || (version==4 && algorithm==35 && [7,14].contains(&packet.tag))) {return Err(wire::invalid());}
29 if version == 6 && algorithm == 17 {
30 return Err(wire::invalid());
31 }
32 let compat = match version {
33 2 | 3 | 5 => true,
34 4 | 6 => false,
35 _ => return Err(wire::invalid()),
36 };
37 if family.is_some_and(|f| f != compat) {
38 return Err(wire::invalid());
39 }
40 family = Some(compat);
41 }
42 Ok(family.unwrap_or(false))
45pub fn describe_public(text: &str) -> CoreResult<Value> {
46 let cert = certificate::Certificate::parse(text.as_bytes())?;
47 if cert.packets.iter().any(|p| [5, 7].contains(&p.tag)) {
48 return Err(wire::invalid());
49 }
50 cert.metadata()
53pub fn parse(bytes: &[u8], label: &str) -> CoreResult<Value> {
54 let cert = certificate::Certificate::parse(bytes)?;
55 let details = cert.metadata()?;
56 if details["bindingValid"] != true {
57 return Err(wire::invalid());
58 }
59 let has_private = cert.packets[0].tag == 5;
60 if bytes.starts_with(b"-----BEGIN PGP PUBLIC KEY BLOCK-----") && has_private {
61 return Err(wire::invalid());
62 }
63 let label = if label.trim().is_empty() {
64 details["userIds"][0]
65 .as_str()
66 .unwrap_or("OpenPGP")
67 .to_owned()
68 } else {
69 label.trim().to_owned()
70 };
71 Ok(
72 json!({"id":uuid::Uuid::new_v4().to_string(),"kind":"openpgp","label":label,
73 "public":cert.public_armor()?,"private":if has_private {Some(cert.private_armor()?)} else {None},
74 "fingerprint":details["fingerprint"],"revoked":details["revoked"],
75 "createdAt":chrono::Utc::now().to_rfc3339()}),
76 )
79pub fn generate(
80 payload: &Value,
81 uid: &str,
82 label: &str,
83 progress: &dyn Fn(&str) -> CoreResult<()>,
84) -> CoreResult<(Value, Value)> {
85 let (cert, revocation) = match payload["version"].as_u64() {
86 Some(2 | 3) => historical::build(payload, uid, progress)?,
87 Some(5) => librepgp::build(payload, uid, progress)?,
88 _ => return Err(wire::invalid()),
89 };
90 let private = zeroize::Zeroizing::new(cert.private_armor()?);
91 let entry = parse(private.as_bytes(), label)?;
92 let output =
93 crate::core::openpgp::artifacts(&entry, &uuid::Uuid::new_v4().to_string(), &revocation);
94 Ok((output, entry))
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use serde_json::json;
101 #[test]
102 fn profiles_generate_actual_packets_and_protect_every_secret() {
103 for version in [2, 3, 5] {
104 let payload = json!({"version":version,"legacyConfirmed":true,"algorithm":if version==5{"ed25519"}else{"rsa1024"},"primaryCapabilities":if version==5{"CS"}else{"CSE"},"passphrase":"synthetic-test-password"});
105 let (cert, revocation) = if version == 5 {
106 librepgp::build(&payload, "Synthetic compatibility fixture", &|_| Ok(())).unwrap()
107 } else {
108 historical::build(&payload, "Synthetic compatibility fixture", &|_| Ok(())).unwrap()
109 };
110 let meta = cert.metadata().unwrap();
111 assert_eq!(meta["version"], version);
112 assert_eq!(meta["usable"], true);
113 assert_eq!(
114 certificate::Certificate::parse(revocation.as_bytes())
115 .unwrap()
116 .metadata()
117 .unwrap()["revoked"],
118 true
119 );
120 for p in cert.packets.iter().filter(|p| [5, 7].contains(&p.tag)) {
121 let key = key::Key::parse(p).unwrap();
122 assert_ne!(key.secret.as_ref().unwrap()[0], 0);
123 assert!(protect::open(&key, "wrong").is_err());
124 let plain = protect::open(&key, "synthetic-test-password").unwrap();
125 key.parameters()
126 .unwrap()
127 .with_secret(key.algorithm, &plain)
128 .unwrap();
129 }
130 }
131 }
133 #[tokio::test]
134 async fn generator_keychain_lifecycle_uses_real_versions_and_preserves_vault_boundary() {
135 use crate::core::{keychain, openpgp, vault::VaultStore};
136 let root = tempfile::tempdir().unwrap();
137 let path = root.path().join("payload");
138 std::fs::write(&path, b"synthetic test file").unwrap();
139 let vault = VaultStore::new(root.path());
140 vault.create("synthetic-vault-password").await.unwrap();
141 for version in [2, 3, 5] {
142 let payload = json!({"name":"Synthetic integration fixture","version":version,"legacyConfirmed":true,"algorithm":if version==5{"ed25519"}else{"rsa2048"},"primaryCapabilities":if version==5{"C"}else{"CSE"},"passphrase":"synthetic-password","subkeys":if version==5{json!([{"role":"sign","algorithm":"ed25519"}])}else{json!([])}});
143 let (output, entry) = openpgp::generate(&payload).unwrap();
144 let meta = keychain::describe(&entry);
145 assert_eq!(meta["version"], version);
146 assert_eq!(meta["usable"], true);
147 assert_eq!(meta["keyserverSupported"], false);
148 assert_eq!(meta["signSupported"], true);
149 assert!(!meta.to_string().contains("PRIVATE KEY"));
150 assert!(crate::core::keyserver::public_payload(&entry).is_err());
151 assert!(keychain::sign(&entry, &path, "wrong").is_err());
152 let sig = keychain::sign(&entry, &path, "synthetic-password").unwrap();
153 keychain::verify(&entry, &path, sig.as_bytes()).unwrap();
154 std::fs::write(&path, b"tampered").unwrap();
155 assert!(keychain::verify(&entry, &path, sig.as_bytes()).is_err());
156 std::fs::write(&path, b"synthetic test file").unwrap();
157 let parsed = keychain::parse(
158 entry["private"].as_str().unwrap().as_bytes(),
159 "Imported",
160 "",
161 )
162 .unwrap();
163 assert_eq!(parsed["fingerprint"], entry["fingerprint"]);
164 let public =
165 keychain::parse(entry["public"].as_str().unwrap().as_bytes(), "Public", "")
166 .unwrap();
167 assert!(public["private"].is_null());
168 let revoked = keychain::revoke(&entry, "synthetic-password").unwrap();
169 assert_eq!(keychain::describe(&revoked)["revoked"], true);
170 assert!(keychain::verify(&revoked, &path, sig.as_bytes()).is_err());
171 let rev = keychain::parse(
172 output["files"][2]["content"].as_str().unwrap().as_bytes(),
173 "Revocation",
174 "",
175 )
176 .unwrap();
177 assert_eq!(rev["revoked"], true);
178 let stored = keychain::store(&vault, entry.clone()).unwrap();
179 assert_eq!(stored["version"], version);
180 assert!(stored.get("private").is_none());
181 }
182 vault.lock();
183 assert!(keychain::list(&vault).is_err());
184 vault.unlock("synthetic-vault-password").await.unwrap();
185 assert_eq!(keychain::list(&vault).unwrap().as_array().unwrap().len(), 3);
186 }
187 #[test]
188 fn v5_edits_preserve_password_fingerprint_and_revocation() {
189 use crate::core::{keychain, openpgp};
190 let expires = pgp::types::Timestamp::now().as_secs() as u64 + 86400;
191 let (_,entry)=openpgp::generate(&json!({"name":"Synthetic edit fixture","version":5,"passphrase":"fixture-password","subkeys":[]})).unwrap();
192 let source =
193 certificate::Certificate::parse(entry["private"].as_str().unwrap().as_bytes()).unwrap();
194 assert!(openpgp::edit(&entry,&json!({"operation":"addSubkey","passphrase":"wrong","subkey":{"role":"sign","algorithm":"ed25519"}}),&|_|Ok(())).is_err());
195 let added=openpgp::edit(&entry,&json!({"operation":"addSubkey","passphrase":"fixture-password","subkey":{"role":"sign","algorithm":"ed25519","expiresAt":expires}}),&|_|Ok(())).unwrap();
196 let updated =
197 certificate::Certificate::parse(added["private"].as_str().unwrap().as_bytes()).unwrap();
198 assert_eq!(source.packets[0].body, updated.packets[0].body);
199 assert_eq!(entry["fingerprint"], added["fingerprint"]);
200 let details = keychain::describe(&added);
201 assert_eq!(details["subkeys"][0]["effectiveCapabilities"], "S");
202 let fp = details["subkeys"][0]["fingerprint"].clone();
203 let renewed=openpgp::edit(&added,&json!({"operation":"setExpiration","passphrase":"fixture-password","expiresAt":expires}),&|_|Ok(())).unwrap();
204 assert_eq!(keychain::describe(&renewed)["expiresAt"], expires);
205 let renewed=openpgp::edit(&renewed,&json!({"operation":"setExpiration","fingerprint":fp,"passphrase":"fixture-password","expiresAt":expires+86400}),&|_|Ok(())).unwrap();
206 assert_eq!(
207 keychain::describe(&renewed)["subkeys"][0]["expiresAt"],
208 expires + 86400
209 );
210 let revoked=openpgp::edit(&renewed,&json!({"operation":"revokeSubkey","fingerprint":fp,"confirmed":true,"passphrase":"fixture-password"}),&|_|Ok(())).unwrap();
211 assert_eq!(keychain::describe(&revoked)["subkeys"][0]["revoked"], true);
212 assert!(openpgp::edit(&revoked,&json!({"operation":"setExpiration","fingerprint":fp,"expiresAt":expires,"passphrase":"fixture-password"}),&|_|Ok(())).is_err());
213 }
214 #[test]
215 fn legacy_confirmation_and_capabilities_cannot_be_bypassed() {
216 use crate::core::openpgp;
217 for version in [2, 3] {
218 let mut p = json!({"name":"Synthetic legacy fixture","version":version,"algorithm":"rsa1024","subkeys":[]});
219 assert!(openpgp::generate(&p).is_err());
220 p["legacyConfirmed"] = json!(true);
221 for field in [
222 json!({"primaryCapabilities":"CSA"}),
223 json!({"subkeys":[{"role":"sign"}]}),
224 json!({"name":"Non-ASCII ž"}),
225 json!({"passphrase":"Non-ASCII ž"}),
226 json!({"algorithm":"ed25519"}),
227 ] {
228 let mut bad = p.clone();
229 for (key, value) in field.as_object().unwrap() {
230 bad[key] = value.clone();
231 }
232 assert!(openpgp::generate(&bad).is_err());
233 }
234 let mut finite = p.clone();
235 finite["expiresAt"] = json!(pgp::types::Timestamp::now().as_secs() as u64 + 300);
236 if version == 2 {
237 assert!(openpgp::generate(&finite).is_err());
238 } else {
239 let (out, _) = openpgp::generate(&finite).unwrap();
240 assert_eq!(
241 out["identity"]["expiresAt"].as_u64().unwrap()
242 - out["identity"]["keyCreatedAt"].as_u64().unwrap(),
243 86400
244 );
245 }
246 let (out, _) = openpgp::generate(&p).unwrap();
247 assert_eq!(out["identity"]["derivedCapabilities"], "CSE");
248 assert_eq!(out["identity"]["declaredCapabilities"], "");
249 assert_eq!(out["identity"]["editSupported"], false);
250 }
251 }
252 #[test]
253 fn malformed_or_mixed_packets_never_enter_the_modern_parser() {
254 use wire::*;
255 for version in [2, 3, 5] {
256 let payload = json!({"version":version,"legacyConfirmed":true,"algorithm":if version==5{"ed25519"}else{"rsa1024"}});
257 let (cert, _) = if version == 5 {
258 librepgp::build(&payload, "Synthetic parser fixture", &|_| Ok(())).unwrap()
259 } else {
260 historical::build(&payload, "Synthetic parser fixture", &|_| Ok(())).unwrap()
261 };
262 let raw = encode(&cert.public_packets().unwrap(), version).unwrap();
263 for end in 0..raw.len() {
264 let _ = crate::core::keychain::parse(&raw[..end], "", "");
265 }
266 let mut packets = cert.public_packets().unwrap();
267 packets.push(Packet::new(14, vec![6, 0]));
268 assert!(handles(&encode(&packets, 5).unwrap()).is_err());
269 let mut packets = cert.public_packets().unwrap();
270 let sig = packets.iter_mut().find(|p| p.tag == 2).unwrap();
271 let last = sig.body.len() - 1;
272 sig.body[last] ^= 1;
273 let forged = armor("PUBLIC KEY BLOCK", &encode(&packets, version).unwrap());
274 assert!(crate::core::keychain::parse(forged.as_bytes(), "", "").is_err());
275 let hidden = armor("PUBLIC KEY BLOCK", &encode(&cert.packets, version).unwrap());
276 assert!(crate::core::keychain::parse(hidden.as_bytes(), "", "").is_err());
277 }
278 }
279 #[test]
280 #[ignore = "explicit independently generated signatures in a synthetic fixture directory"]
281 fn verify_independent_compatibility_signatures() {
282 let root = std::path::PathBuf::from(std::env::var("CHJ_COMPAT_FIXTURES").unwrap());
283 for version in [2, 3, 5] {
284 let public = std::fs::read(root.join(format!("v{version}-public.asc"))).unwrap();
285 let entry = crate::core::keychain::parse(&public, "Independent fixture", "").unwrap();
286 let signature =
287 std::fs::read(root.join(format!("v{version}-gpg-signature.asc"))).unwrap();
288 crate::core::keychain::verify(&entry, &root.join("payload.txt"), &signature).unwrap();
289 }
290 }
291 #[cfg(unix)]
292 #[test]
293 #[ignore = "explicit synthetic fixture export for independent interoperability checks"]
294 fn export_interoperability_fixtures() {
295 use std::io::Write;
296 use std::os::unix::fs::{DirBuilderExt, OpenOptionsExt};
297 let root = std::path::PathBuf::from(
298 std::env::var("CHJ_COMPAT_FIXTURES").expect("explicit fixture directory"),
299 );
300 std::fs::DirBuilder::new()
301 .mode(0o700)
302 .create(&root)
303 .unwrap();
304 for version in [2, 3, 5] {
305 let payload = json!({"name":"Synthetic compatibility fixture","version":version,"legacyConfirmed":true,"algorithm":if version==5{"ed25519"}else{"rsa1024"},"primaryCapabilities":if version==5{"CS"}else{"CSE"},"passphrase":"synthetic-test-password","subkeys":if version==5{json!([{"role":"encrypt","algorithm":"curve25519"},{"role":"sign","algorithm":"ed25519"}])}else{json!([])}});
306 let mut payload = payload;
307 if version == 5 {
308 if let Ok(algorithm) = std::env::var("CHJ_COMPAT_PRIMARY") {
309 payload["algorithm"] = json!(algorithm);
310 }
311 }
312 let (out, entry) = crate::core::openpgp::generate(&payload).unwrap();
313 assert_eq!(out["identity"]["version"], version);
314 for file in out["files"].as_array().unwrap() {
315 let path = root.join(format!("v{version}-{}.asc", file["kind"].as_str().unwrap()));
316 let mut f = std::fs::OpenOptions::new()
317 .write(true)
318 .create_new(true)
319 .mode(0o600)
320 .open(path)
321 .unwrap();
322 f.write_all(file["content"].as_str().unwrap().as_bytes())
323 .unwrap();
324 }
325 let data = root.join("payload.txt");
326 std::fs::write(&data, b"Synthetic interoperability payload\n").unwrap();
327 let signature =
328 crate::core::keychain::sign(&entry, &data, "synthetic-test-password").unwrap();
329 std::fs::write(root.join(format!("v{version}-signature.asc")), signature).unwrap();
330 if version == 5 {
331 let expires = pgp::types::Timestamp::now().as_secs() as u64 + 86400;
332 let added=crate::core::openpgp::edit(&entry,&json!({"operation":"addSubkey","passphrase":"synthetic-test-password","subkey":{"role":"authenticate","algorithm":"ed25519"}}),&|_|Ok(())).unwrap();
333 let renewed=crate::core::openpgp::edit(&added,&json!({"operation":"setExpiration","passphrase":"synthetic-test-password","expiresAt":expires}),&|_|Ok(())).unwrap();
334 let fp =
335 crate::core::keychain::describe(&renewed)["subkeys"][1]["fingerprint"].clone();
336 let revoked=crate::core::openpgp::edit(&renewed,&json!({"operation":"revokeSubkey","fingerprint":fp,"confirmed":true,"passphrase":"synthetic-test-password"}),&|_|Ok(())).unwrap();
337 std::fs::write(
338 root.join("v5-edited-public.asc"),
339 revoked["public"].as_str().unwrap(),
340 )
341 .unwrap();
342 }
343 std::fs::write(
344 root.join(format!("v{version}-metadata.json")),
345 serde_json::to_vec_pretty(&crate::core::keychain::describe(&entry)).unwrap(),
346 )
347 .unwrap();
348 }
349 }
SHA-256968022af3fb8b454320a80ba4d2f3669908662cc53933d6a312d77751ea8b42b

CH-J Proprietary Software License 1.14 · La sorgente è fornita sotto CH-J Proprietary Software License 1.14. La sua disponibilità non modifica i termini di licenza o concede diritti aggiuntivi.

Archivio SHA-256ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f
File