CH-J Tools通过 SSH 管理服务器
菜单
已发布的源代码

CH-J Tools

浏览特定应用版本的目录和文件。

下载源代码 ZIP
CH-J / Server Manager 1704 个文件
0.1.2-beta 添加时间: · 构建时间:
openpgp.rs 添加时间: 未记录日期 62.0 KB · 1384 行
代码 下载文件
1//! Local OpenPGP generation and certificate descriptions. No external GnuPG/home/keyserver.
2use crate::core::error::{CoreError, CoreResult};
3use pgp::{
4 composed::{
5 Deserializable, EncryptionCaps, KeyType, SecretKeyParamsBuilder, SignedPublicKey,
6 SignedSecretKey, SignedSecretSubKey,
7 },
8 crypto::ecc_curve::ECCCurve,
9 packet::{SignatureConfig, SignatureType, Subpacket, SubpacketData},
10 types::{KeyDetails, KeyVersion, Password, Timestamp},
11};
12use serde_json::{json, Value};
13use zeroize::Zeroizing;
14fn invalid(message: &str) -> CoreError {
15 CoreError::new("OPENPGP_INPUT_INVALID", message)
18pub fn capabilities_from_bits(bits: u8) -> String {
19 [('C', 0x01), ('S', 0x02), ('A', 0x20), ('E', 0x0c)]
20 .iter()
21 .filter_map(|(c, bit)| (bits & bit != 0).then_some(*c))
22 .collect()
24fn allowed_capabilities(
25 algorithm: pgp::crypto::public_key::PublicKeyAlgorithm,
26 primary: bool,
27) -> String {
28 use pgp::crypto::public_key::PublicKeyAlgorithm as A;
29 if algorithm == A::DSA { return if primary {"CS"} else {"S"}.into(); }
30 let (sign, encrypt) = match algorithm {
31 A::RSA => (true, true),
32 A::RSAEncrypt | A::ElgamalEncrypt | A::ECDH | A::X25519 | A::X448 | A::MlKem768X25519 | A::MlKem1024X448 => (false, true),
33 A::RSASign | A::ECDSA | A::EdDSALegacy | A::Ed25519 | A::Ed448 | A::MlDsa65Ed25519 | A::MlDsa87Ed448 | A::SlhDsaShake128s | A::SlhDsaShake128f | A::SlhDsaShake256s => (true, false),
34 _ => (false, false),
35 };
36 format!(
37 "{}{}{}{}",
38 if primary && sign { "C" } else { "" },
39 if sign { "S" } else { "" },
40 if sign { "A" } else { "" },
41 if encrypt { "E" } else { "" }
42 )
44fn flag_bits(signature: &pgp::packet::Signature) -> Option<u8> {
45 signature
46 .config()?
47 .hashed_subpackets()
48 .find_map(|p| match &p.data {
49 pgp::packet::SubpacketData::KeyFlags(flags) => Some(
50 (if flags.certify() { 1 } else { 0 })
51 | (if flags.sign() { 2 } else { 0 })
52 | (if flags.authentication() { 0x20 } else { 0 })
53 | (if flags.encrypt_comms() { 4 } else { 0 })
54 | (if flags.encrypt_storage() { 8 } else { 0 }),
55 ),
56 _ => None,
57 })
59fn describe_component(
60 key: &impl KeyDetails,
61 primary: bool,
62 binding: Option<&pgp::packet::Signature>,
63 revoked: bool,
64 parent_usable: bool,
65 binding_verified: bool,
66 now: std::time::SystemTime,
67) -> Value {
68 use crate::core::updates::openpgp::{key_unexpired, signature_current, system_time};
69 let version: u8 = key.version().into();
70 let allowed = allowed_capabilities(key.algorithm(), primary);
71 let flags = if version >= 4 {
72 binding.and_then(flag_bits)
73 } else {
74 None
75 };
76 let declared = flags.map(capabilities_from_bits).unwrap_or_default();
77 // A is an explicit intent, never inferred from historical RSA or missing flags.
78 let derived: String = if flags.is_none() {
79 allowed.chars().filter(|c| *c != 'A').collect()
80 } else {
81 String::new()
82 };
83 let requested = if flags.is_some() { &declared } else { &derived };
84 let created = system_time(key.created_at());
85 let expired = binding.is_some_and(|sig| !signature_current(sig, now))
86 || if version <= 3 {
87 key.legacy_v3_expiration_days().is_some_and(|days| {
88 days > 0 && created + std::time::Duration::from_secs(days as u64 * 86400) <= now
89 })
90 } else {
91 binding.is_some_and(|sig| !key_unexpired(created, sig, now))
92 };
93 let usable = binding_verified
94 && parent_usable
95 && !revoked
96 && !expired
97 && created <= now + std::time::Duration::from_secs(300)
98 && binding.is_some_and(|sig| signature_current(sig, now))
99 && version != 5
100 && !(version == 6 && key.algorithm() == pgp::crypto::public_key::PublicKeyAlgorithm::DSA)
101 && !(version <= 3 && !primary);
102 let effective: String = if usable {
103 requested.chars().filter(|c| allowed.contains(*c)).collect()
104 } else {
105 String::new()
106 };
107 let expires_at = if version <= 3 {
108 key.legacy_v3_expiration_days()
109 .filter(|d| *d > 0)
110 .map(|d| key.created_at().as_secs() as u64 + d as u64 * 86400)
111 } else {
112 binding
113 .and_then(|s| s.key_expiration_time())
114 .filter(|d| d.as_secs() > 0)
115 .map(|d| key.created_at().as_secs() as u64 + d.as_secs() as u64)
116 };
117 let mut details=json!({"keyCreatedAt":key.created_at().as_secs(),"expiresAt":expires_at,"fingerprint":hex::encode_upper(key.fingerprint().as_ref()),"version":version,"keyId":hex::encode_upper(key.legacy_key_id().as_ref()),"algorithm":format!("{:?}",key.algorithm()),"algorithmId":u8::from(key.algorithm()),"postQuantum":key.algorithm().is_pqc(),"hybrid":([30u8,31,35,36].contains(&u8::from(key.algorithm()))),"declaredCapabilities":declared,"derivedCapabilities":derived,"effectiveCapabilities":effective,"capabilitySource":if flags.is_some(){"declared"}else{"derived"},"keyFlagBits":flags,"revoked":revoked,"expired":expired,"bindingValid":binding_verified,"usable":usable});
118 for (name,value) in super::openpgp_algorithms::details(key.public_params()).as_object().unwrap(){details[name]=value.clone();}
119 details
121pub fn describe_public(text: &str) -> CoreResult<Value> {
122 if super::openpgp_compat::handles(text.as_bytes())? {
123 return super::openpgp_compat::describe_public(text);
124 }
125 use crate::core::updates::openpgp::system_time;
126 use pgp::{packet::SignatureType, types::Tag};
127 let (key, _) = SignedPublicKey::from_reader_single(std::io::Cursor::new(text))
128 .map_err(|_| invalid("Invalid OpenPGP certificate."))?;
129 let now = std::time::SystemTime::now();
130 let eligible = |sig: &&pgp::packet::Signature| {
131 sig.created().is_some_and(|created| {
132 system_time(created) <= now + std::time::Duration::from_secs(300)
133 })
134 };
135 let mut self_signatures: Vec<&pgp::packet::Signature> = key
136 .details
137 .direct_signatures
138 .iter()
139 .filter(|sig| {
140 sig.typ() == Some(SignatureType::Key) && sig.verify_key(&key.primary_key).is_ok()
141 })
142 .collect();
143 for user in &key.details.users {
144 let revoked_at = user
145 .signatures
146 .iter()
147 .filter(|sig| {
148 sig.typ() == Some(SignatureType::CertRevocation)
149 && sig
150 .verify_certification(&key.primary_key, Tag::UserId, &user.id)
151 .is_ok()
152 })
153 .filter(eligible)
154 .filter_map(|sig| sig.created().map(system_time))
155 .max();
156 for sig in &user.signatures {
157 if !matches!(
158 sig.typ(),
159 Some(
160 SignatureType::CertGeneric
161 | SignatureType::CertPersona
162 | SignatureType::CertCasual
163 | SignatureType::CertPositive
164 )
165 ) {
166 continue;
167 }
168 if key.primary_key.version() != pgp::types::KeyVersion::V6
169 && sig
170 .verify_certification(&key.primary_key, Tag::UserId, &user.id)
171 .is_ok()
172 && revoked_at.is_none_or(|at| {
173 sig.created()
174 .is_some_and(|created| system_time(created) > at)
175 })
176 {
177 self_signatures.push(sig);
178 }
179 }
180 }
181 let binding = self_signatures
182 .into_iter()
183 .filter(eligible)
184 .max_by_key(|sig| {
185 (
186 sig.created().map(system_time),
187 sig.typ() == Some(SignatureType::Key),
188 )
189 });
190 let revoked = key
191 .details
192 .revocation_signatures
193 .iter()
194 .any(|sig| sig.verify_key(&key.primary_key).is_ok());
195 let primary = describe_component(
196 &key.primary_key,
197 true,
198 binding,
199 revoked,
200 true,
201 binding.is_some(),
202 now,
203 );
204 let subkeys: Vec<Value> = key
205 .public_subkeys
206 .iter()
207 .map(|sub| {
208 let binding = sub
209 .signatures
210 .iter()
211 .filter(|sig| {
212 sig.typ() == Some(SignatureType::SubkeyBinding)
213 && sig
214 .verify_subkey_binding(&key.primary_key, &sub.key)
215 .is_ok()
216 })
217 .filter(eligible)
218 .max_by_key(|sig| sig.created().map(system_time));
219 let revoked = sub.signatures.iter().any(|sig| {
220 sig.typ() == Some(SignatureType::SubkeyRevocation)
221 && sig
222 .verify_subkey_binding(&key.primary_key, &sub.key)
223 .is_ok()
224 });
225 let verified = sub.key.version() == key.primary_key.version()
226 && binding.is_some_and(|sig| {
227 !flag_bits(sig).map(|bits| bits & 2 != 0).unwrap_or_else(|| {
228 allowed_capabilities(sub.key.algorithm(), false).contains('S')
229 }) || sig.embedded_signature().is_some_and(|back| {
230 back.verify_primary_key_binding(&sub.key, &key.primary_key)
231 .is_ok()
232 && crate::core::updates::openpgp::signature_current(back, now)
233 })
234 });
235 let mut metadata = describe_component(
236 &sub.key,
237 false,
238 binding,
239 revoked,
240 primary["usable"] == true
241 && primary["effectiveCapabilities"]
242 .as_str()
243 .unwrap_or("")
244 .contains('C'),
245 verified,
246 now,
247 );
248 let effective = metadata["effectiveCapabilities"].as_str().unwrap_or("");
249 let roles: Vec<&str> = [('S', "sign"), ('A', "authenticate"), ('E', "encrypt")]
250 .into_iter()
251 .filter_map(|(c, role)| effective.contains(c).then_some(role))
252 .collect();
253 metadata["roles"] = json!(roles);
254 metadata
255 })
256 .collect();
257 let mut aggregate = String::new();
258 for capability in "CSAE".chars() {
259 if std::iter::once(&primary).chain(subkeys.iter()).any(|meta| {
260 meta["effectiveCapabilities"]
261 .as_str()
262 .unwrap_or("")
263 .contains(capability)
264 }) {
265 aggregate.push(capability);
266 }
267 }
268 let user_ids: Vec<String> = key
269 .details
270 .users
271 .iter()
272 .map(|u| String::from_utf8_lossy(u.id.id()).to_string())
273 .collect();
274 let mut result = primary;
275 result["userIdentityDetails"] = json!(key.details.users.iter().map(|u| {
276 let revoked=u.signatures.iter().any(|sig|sig.typ()==Some(SignatureType::CertRevocation) && eligible(&sig) && sig.verify_certification(&key.primary_key,Tag::UserId,&u.id).is_ok());
277 json!({"userId":String::from_utf8_lossy(u.id.id()),"revoked":revoked})
278 }).collect::<Vec<_>>());
279 result["userIds"] = json!(user_ids);
280 result["subkeys"] = json!(subkeys);
281 result["certificateCapabilities"] = json!(aggregate);
282 Ok(result)
285pub fn file_base(label: &str, fingerprint: &str) -> String {
286 let label: String = label
287 .chars()
288 .filter(|c| !c.is_control())
289 .map(|c| if "/\\:<>\"|?*".contains(c) { '_' } else { c })
290 .take(80)
291 .collect();
292 let label = label.trim().trim_matches('.');
293 let short: String = fingerprint
294 .chars()
295 .rev()
296 .take(8)
297 .collect::<String>()
298 .chars()
299 .rev()
300 .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
301 .collect();
302 format!(
303 "{} ({})",
304 if label.is_empty() { "OpenPGP" } else { label },
305 short
306 )
308fn validate_capabilities(algorithm: &str, caps: &str) -> CoreResult<()> {
309 // Actual version is validated separately; this shared catalog also rejects unknown flags.
310 if [2,3,4,5,6].iter().any(|v|super::openpgp_algorithms::validate(*v,algorithm,caps).is_ok()){Ok(())}
311 else{Err(invalid("The selected algorithm cannot provide these capabilities."))}
313fn key_type(
314 algorithm: &str,
315 encrypt: bool,
316 version: pgp::types::KeyVersion,
317) -> CoreResult<KeyType> {
318 match algorithm {
319 "ed25519" if !encrypt => Ok(if version == pgp::types::KeyVersion::V6 {
320 KeyType::Ed25519
321 } else {
322 KeyType::Ed25519Legacy
323 }),
324 "curve25519" if encrypt => Ok(if version == pgp::types::KeyVersion::V6 {
325 KeyType::X25519
326 } else {
327 KeyType::ECDH(ECCCurve::Curve25519Legacy)
328 }),
329 "ml-dsa65-ed25519" if version==KeyVersion::V6 => Ok(KeyType::MlDsa65Ed25519),
330 "ml-dsa87-ed448" if version==KeyVersion::V6 => Ok(KeyType::MlDsa87Ed448),
331 "slh-dsa-shake128s" if version==KeyVersion::V6 => Ok(KeyType::SlhDsaShake128s),
332 "slh-dsa-shake128f" if version==KeyVersion::V6 => Ok(KeyType::SlhDsaShake128f),
333 "slh-dsa-shake256s" if version==KeyVersion::V6 => Ok(KeyType::SlhDsaShake256s),
334 "ml-kem768-x25519" if [KeyVersion::V4,KeyVersion::V6].contains(&version) => Ok(KeyType::MlKem768X25519),
335 "ml-kem1024-x448" if version==KeyVersion::V6 => Ok(KeyType::MlKem1024X448),
336 "rsa3072" => Ok(KeyType::Rsa(3072)),
337 "rsa4096" => Ok(KeyType::Rsa(4096)),
338 _ => Err(invalid(
339 "Choose a supported OpenPGP algorithm for this key capability.",
340 )),
341 }
343pub fn generate(payload: &Value) -> CoreResult<(Value, Value)> {
344 generate_progress(payload, &|_| Ok(()))
346fn generate_for_uid_progress(
347 payload: &Value,
348 uid: &str,
349 label: &str,
350 progress: &dyn Fn(&str) -> CoreResult<()>,
351) -> CoreResult<(Value, Value)> {
352 if uid.is_empty() || uid.chars().count() > 400 || uid.chars().any(char::is_control) {
353 return Err(invalid("Invalid OpenPGP user ID."));
354 }
355 let passphrase = Zeroizing::new(payload["passphrase"].as_str().unwrap_or("").to_string());
356 if passphrase.chars().count() > 1024 || passphrase.contains('\0') {
357 return Err(invalid("Invalid key passphrase."));
358 }
359 let requested_version = match payload.get("version") {
360 None => 6,
361 Some(value) => value
362 .as_u64()
363 .ok_or_else(|| invalid("Invalid OpenPGP key version."))?,
364 };
365 let version = match requested_version {
366 4=>pgp::types::KeyVersion::V4,6=>pgp::types::KeyVersion::V6,
367 2|3|5=>return super::openpgp_compat::generate(payload,uid,label,progress),
368 _=>return Err(invalid("Invalid OpenPGP key version.")),
369 };
370 let primary_caps = payload["primaryCapabilities"].as_str().unwrap_or("CS");
371 if !primary_caps.contains('C')
372 || primary_caps.is_empty()
373 || primary_caps.chars().any(|c| !"CSAE".contains(c))
374 {
375 return Err(invalid(
376 "Primary capabilities must contain C and only C/S/A/E.",
377 ));
378 }
380 let specs = payload
381 .get("subkeys")
382 .and_then(Value::as_array)
383 .cloned()
384 .unwrap_or_else(|| vec![json!({"role":"encrypt","algorithm":"curve25519"})]);
385 if specs.len() > 8 {
386 return Err(invalid(
387 "At most eight OpenPGP subkeys can be generated at once.",
388 ));
389 }
390 validate_capabilities(
391 payload["algorithm"].as_str().unwrap_or("ed25519"),
392 primary_caps,
393 )?;
394 expiration_packet(Timestamp::now(), payload)?;
395 for spec in &specs {
396 expiration_packet(Timestamp::now(), spec)?;
397 validate_subkey(requested_version as u8,spec)?;
398 }
399 let algorithm=payload["algorithm"].as_str().unwrap_or("ed25519");
400 super::openpgp_algorithms::validate(requested_version as u8,algorithm,primary_caps)?;
401 progress("primary")?;
402 let mut secret = if super::openpgp_algorithms::is_dsa(algorithm) { generate_dsa_primary(algorithm,uid,primary_caps)? } else { SecretKeyParamsBuilder::default()
403 .version(version)
404 .feature_seipd_v1(version==KeyVersion::V4)
405 .feature_seipd_v2(version==KeyVersion::V6)
406 .preferred_symmetric_algorithms([pgp::crypto::sym::SymmetricKeyAlgorithm::AES256,pgp::crypto::sym::SymmetricKeyAlgorithm::AES128].into_iter().collect())
407 .preferred_aead_algorithms([(pgp::crypto::sym::SymmetricKeyAlgorithm::AES256,pgp::crypto::aead::AeadAlgorithm::Ocb),(pgp::crypto::sym::SymmetricKeyAlgorithm::AES128,pgp::crypto::aead::AeadAlgorithm::Ocb)].into_iter().collect())
408 .key_type(key_type(
409 payload["algorithm"].as_str().unwrap_or("ed25519"),
410 false,
411 version,
412 )?)
413 .can_certify(true)
414 .can_sign(primary_caps.contains('S'))
415 .can_authenticate(primary_caps.contains('A'))
416 .can_encrypt(if primary_caps.contains('E') {
417 EncryptionCaps::All
418 } else {
419 EncryptionCaps::None
420 })
421 .primary_user_id(uid.to_string())
422 .build()
423 .map_err(|_| invalid("Invalid OpenPGP settings."))?
424 .generate(rand::thread_rng())
425 .map_err(|_| invalid("OpenPGP generation failed."))? };
426 apply_expiration(&mut secret, &Password::empty(), None, payload)?;
427 for spec in &specs {
428 progress("subkey")?;
429 let sub = make_subkey(&secret.primary_key, spec)?;
430 secret.secret_subkeys.push(sub);
431 }
432 progress("revocation")?;
433 let revocation = revocation_certificate(&secret, &Password::empty())?;
434 progress("protect")?;
435 if !passphrase.is_empty() {
436 let password = Password::from(passphrase.as_str());
437 secret
438 .primary_key
439 .set_password(&mut rand::thread_rng(), &password)
440 .map_err(|_| invalid("Key protection failed."))?;
441 for sub in &mut secret.secret_subkeys {
442 progress("protect")?;
443 sub.key
444 .set_password(&mut rand::thread_rng(), &password)
445 .map_err(|_| invalid("Subkey protection failed."))?;
446 }
447 }
448 progress("verify")?;
449 let private = Zeroizing::new(
450 secret
451 .to_armored_string(Default::default())
452 .map_err(|_| invalid("OpenPGP export failed."))?,
453 );
454 // Parsing verifies bindings; it never needs to unlock a private packet.
455 let entry = crate::core::keychain::parse(private.as_bytes(), label, "")?;
456 let output = artifacts(&entry, &uuid::Uuid::new_v4().to_string(), &revocation);
457 Ok((output, entry))
460/// A progress callback contains phase names only, never identity or secret material.
461pub fn generate_progress(
462 payload: &Value,
463 progress: &dyn Fn(&str) -> CoreResult<()>,
464) -> CoreResult<(Value, Value)> {
465 // Validate and assemble the user ID using the same entry point without generating twice.
466 let name = payload["name"].as_str().unwrap_or("").trim();
467 let email = payload["email"].as_str().unwrap_or("").trim();
468 let comment = payload["comment"].as_str().unwrap_or("").trim();
469 validate_identity(name, email, comment)?;
470 let uid = format!(
471 "{name}{}{}",
472 if comment.is_empty() {
473 String::new()
474 } else {
475 format!(" ({comment})")
476 },
477 if email.is_empty() {
478 String::new()
479 } else {
480 format!(" <{email}>")
481 }
482 );
483 generate_for_uid_progress(payload, &uid, name, progress)
485fn validate_identity(name: &str, email: &str, comment: &str) -> CoreResult<()> {
486 if name.is_empty()
487 || name.chars().count() > 100
488 || name.chars().any(|c| c.is_control() || "<>".contains(c))
489 {
490 return Err(invalid("Enter a valid OpenPGP identity name."));
491 }
492 if !email.is_empty()
493 && (email.len() > 254
494 || email.matches('@').count() != 1
495 || email.starts_with('@')
496 || email.ends_with('@')
497 || email
498 .chars()
499 .any(|c| c.is_whitespace() || c.is_control() || "<>()".contains(c)))
500 {
501 return Err(invalid("Enter a valid email address, or leave it empty."));
502 }
503 if comment.chars().count() > 100
504 || comment
505 .chars()
506 .any(|c| c.is_control() || "()<>".contains(c))
507 {
508 return Err(invalid("Invalid OpenPGP identity comment."));
509 }
510 Ok(())
512fn packet(data: SubpacketData) -> CoreResult<Subpacket> {
513 Subpacket::regular(data).map_err(|_| invalid("Invalid signature subpacket."))
515fn signature_config(
516 primary: &pgp::packet::SecretKey,
517 typ: SignatureType,
518) -> CoreResult<SignatureConfig> {
519 let mut config = SignatureConfig::from_key(&mut rand::thread_rng(), primary, typ)
520 .map_err(|_| invalid("Unsupported signing key version."))?;
521 config.hash_alg = super::openpgp_algorithms::signing_hash(primary.public_params());
522 config.hashed_subpackets = vec![
523 packet(SubpacketData::SignatureCreationTime(Timestamp::now()))?,
524 packet(SubpacketData::IssuerFingerprint(primary.fingerprint()))?,
525 ];
526 if primary.version() == KeyVersion::V4 {
527 config.unhashed_subpackets =
528 vec![packet(SubpacketData::IssuerKeyId(primary.legacy_key_id()))?];
529 }
530 Ok(config)
532fn generate_dsa_primary(algorithm:&str,uid:&str,caps:&str)->CoreResult<SignedSecretKey> {
533 let (public,secret)=super::openpgp_algorithms::generate_dsa(4,algorithm)?;
534 let inner=pgp::packet::PubKeyInner::new(KeyVersion::V4,pgp::crypto::public_key::PublicKeyAlgorithm::DSA,Timestamp::now(),None,public).map_err(|_|invalid("DSA public key failed."))?;
535 let primary=pgp::packet::SecretKey::new(pgp::packet::PublicKey::from_inner(inner).map_err(|_|invalid("DSA public key failed."))?,secret).map_err(|_|invalid("DSA private key failed."))?;
536 let id=pgp::packet::UserId::from_str(Default::default(),uid).map_err(|_|invalid("Invalid user ID."))?;
537 let mut config=signature_config(&primary,SignatureType::CertPositive)?;
538 let mut flags=pgp::packet::KeyFlags::default();flags.set_certify(caps.contains('C'));flags.set_sign(caps.contains('S'));
539 config.hashed_subpackets.push(packet(SubpacketData::KeyFlags(flags))?);
540 let sig=config.sign_certification(&primary,primary.public_key(),&Password::empty(),pgp::types::Tag::UserId,&id).map_err(|_|invalid("DSA certification failed."))?;
541 Ok(SignedSecretKey::new(primary,pgp::composed::SignedKeyDetails::new(vec![],vec![],vec![id.into_signed(sig)],vec![]),vec![],vec![]))
543fn expiration_packet(created: Timestamp, payload: &Value) -> CoreResult<Option<Subpacket>> {
544 let Some(value) = payload.get("expiresAt") else {
545 return Ok(None);
546 };
547 if value.is_null() || value == 0 {
548 return Ok(None);
549 }
550 let at = value
551 .as_u64()
552 .ok_or_else(|| invalid("Invalid key expiration."))?;
553 let now = Timestamp::now().as_secs() as u64;
554 if at <= now
555 || at <= created.as_secs() as u64
556 || at - created.as_secs() as u64 > u32::MAX as u64
557 {
558 return Err(invalid(
559 "Choose a future expiration within the OpenPGP range.",
560 ));
561 }
562 Ok(Some(packet(SubpacketData::KeyExpirationTime(
563 pgp::types::Duration::from_secs((at - created.as_secs() as u64) as u32),
564 ))?))
566fn refresh_config(
567 config: &mut SignatureConfig,
568 created: Timestamp,
569 payload: &Value,
570) -> CoreResult<()> {
571 // Preserve authenticated flags, preferences and the embedded signing-subkey back signature.
572 let created_at = config
573 .hashed_subpackets
574 .iter()
575 .filter_map(|p| {
576 if let SubpacketData::SignatureCreationTime(t) = p.data {
577 Some(t)
578 } else {
579 None
580 }
581 })
582 .max()
583 .unwrap_or(Timestamp::now())
584 .max(Timestamp::now());
585 config.hashed_subpackets.retain(|p| {
586 !matches!(
587 p.data,
588 SubpacketData::SignatureCreationTime(_)
589 | SubpacketData::KeyExpirationTime(_)
590 | SubpacketData::SignatureExpirationTime(_)
591 )
592 });
593 config
594 .hashed_subpackets
595 .push(packet(SubpacketData::SignatureCreationTime(created_at))?);
596 if let Some(exp) = expiration_packet(created, payload)? {
597 config.hashed_subpackets.push(exp);
598 }
599 Ok(())
601fn validate_subkey(version:u8,spec:&Value)->CoreResult<()> {
602 let role=spec["role"].as_str().unwrap_or("sign");
603 if !["sign","encrypt","authenticate"].contains(&role){return Err(invalid("Invalid subkey capability."))}
604 let caps=spec["capabilities"].as_str().unwrap_or(match role{"encrypt"=>"E","authenticate"=>"A",_=>"S"});
605 if caps.is_empty() || caps.chars().any(|c| !"SAE".contains(c)){return Err(invalid("Subkey capabilities must contain S/A/E."))}
606 let algorithm=spec["algorithm"].as_str().unwrap_or(if role=="encrypt"{"curve25519"}else{"ed25519"});
607 super::openpgp_algorithms::validate(version,algorithm,caps)
609fn make_subkey(primary: &pgp::packet::SecretKey, spec: &Value) -> CoreResult<SignedSecretSubKey> {
610 validate_subkey(primary.version().into(),spec)?;
611 let role = spec["role"].as_str().unwrap_or("sign");
612 if !["sign", "encrypt", "authenticate"].contains(&role) {
613 return Err(invalid("Invalid subkey capability."));
614 }
615 let caps = spec["capabilities"].as_str().unwrap_or(match role {
616 "sign" => "S",
617 "authenticate" => "A",
618 _ => "E",
619 });
620 if caps.is_empty() || caps.chars().any(|c| !"SAE".contains(c)) {
621 return Err(invalid("Subkey capabilities must contain S/A/E."));
622 }
623 let algorithm = spec["algorithm"].as_str().unwrap_or(if role == "encrypt" {
624 "curve25519"
625 } else {
626 "ed25519"
627 });
628 validate_capabilities(algorithm, caps)?;
629 let version:u8=primary.version().into();
630 super::openpgp_algorithms::validate(version,algorithm,caps)?;
631 let (public, secret) = if super::openpgp_algorithms::is_dsa(algorithm) {
632 super::openpgp_algorithms::generate_dsa(version,algorithm)?
633 } else {key_type(algorithm, algorithm == "curve25519", primary.version())?.generate(&mut rand::thread_rng()).map_err(|_| invalid("Subkey generation failed."))?};
634 let public_algorithm=if super::openpgp_algorithms::is_dsa(algorithm){pgp::crypto::public_key::PublicKeyAlgorithm::DSA}else{key_type(algorithm, algorithm == "curve25519", primary.version())?.to_alg()};
635 let inner = pgp::packet::PubKeyInner::new(
636 primary.version(),
637 public_algorithm,
638 Timestamp::now(),
639 None,
640 public,
641 )
642 .map_err(|_| invalid("Subkey generation failed."))?;
643 let public = pgp::packet::PublicSubkey::from_inner(inner)
644 .map_err(|_| invalid("Subkey generation failed."))?;
645 let sub = pgp::packet::SecretSubkey::new(public, secret)
646 .map_err(|_| invalid("Subkey generation failed."))?;
647 let mut config = signature_config(primary, SignatureType::SubkeyBinding)?;
648 let mut flags = pgp::packet::KeyFlags::default();
649 flags.set_sign(caps.contains('S'));
650 flags.set_authentication(caps.contains('A'));
651 flags.set_encrypt_comms(caps.contains('E'));
652 flags.set_encrypt_storage(caps.contains('E'));
653 config
654 .hashed_subpackets
655 .push(packet(SubpacketData::KeyFlags(flags))?);
656 if let Some(exp) = expiration_packet(sub.created_at(), spec)? {
657 config.hashed_subpackets.push(exp);
658 }
659 if caps.contains('S') {
660 let back=if sub.algorithm()==pgp::crypto::public_key::PublicKeyAlgorithm::DSA {
661 let mut back_config=SignatureConfig::from_key(&mut rand::thread_rng(),&sub,SignatureType::KeyBinding).map_err(|_|invalid("Subkey back signature failed."))?;
662 back_config.hash_alg=super::openpgp_algorithms::signing_hash(sub.public_params());
663 back_config.hashed_subpackets=vec![packet(SubpacketData::SignatureCreationTime(Timestamp::now()))?,packet(SubpacketData::IssuerFingerprint(sub.fingerprint()))?];
664 back_config.unhashed_subpackets=vec![packet(SubpacketData::IssuerKeyId(sub.legacy_key_id()))?];
665 back_config.sign_primary_key_binding(&sub,sub.public_key(),&Password::empty(),primary.public_key()).map_err(|_|invalid("Subkey back signature failed."))?
666 }else{sub.sign_primary_key_binding(&mut rand::thread_rng(),primary.public_key(),&Password::empty()).map_err(|_|invalid("Subkey back signature failed."))?};
667 config
668 .hashed_subpackets
669 .push(packet(SubpacketData::EmbeddedSignature(Box::new(back)))?);
670 }
671 let binding = config
672 .sign_subkey_binding(
673 primary,
674 primary.public_key(),
675 &Password::empty(),
676 sub.public_key(),
677 )
678 .map_err(|_| invalid("Subkey binding failed."))?;
679 Ok(SignedSecretSubKey::new(sub, vec![binding]))
681fn renew_signature(
682 primary: &pgp::packet::SecretKey,
683 previous: &pgp::packet::Signature,
684) -> CoreResult<SignatureConfig> {
685 let previous_config = previous
686 .config()
687 .ok_or_else(|| invalid("Invalid previous signature."))?;
688 let mut config = signature_config(
689 primary,
690 previous
691 .typ()
692 .ok_or_else(|| invalid("Invalid signature type."))?,
693 )?;
694 config.hashed_subpackets = previous_config.hashed_subpackets.clone();
695 config.unhashed_subpackets = previous_config.unhashed_subpackets.clone();
696 Ok(config)
698fn apply_expiration(
699 secret: &mut SignedSecretKey,
700 password: &Password,
701 target: Option<&str>,
702 payload: &Value,
703) -> CoreResult<()> {
704 if payload.get("expiresAt").is_none() {
705 return Ok(());
706 }
707 let primary = &secret.primary_key;
708 if let Some(fp) = target {
709 let (public, signatures) = if let Some(sub) = secret
710 .secret_subkeys
711 .iter_mut()
712 .find(|s| hex::encode_upper(s.key.fingerprint().as_ref()) == fp)
713 {
714 (sub.key.public_key().clone(), &mut sub.signatures)
715 } else {
716 let sub = secret
717 .public_subkeys
718 .iter_mut()
719 .find(|s| hex::encode_upper(s.key.fingerprint().as_ref()) == fp)
720 .ok_or_else(|| invalid("Subkey was not found."))?;
721 (sub.key.clone(), &mut sub.signatures)
722 };
723 if signatures
724 .iter()
725 .any(|s| s.typ() == Some(SignatureType::SubkeyRevocation))
726 {
727 return Err(invalid("A revoked subkey cannot be renewed."));
728 }
729 let previous = signatures
730 .iter()
731 .filter(|s| s.typ() == Some(SignatureType::SubkeyBinding))
732 .max_by_key(|s| s.created())
733 .ok_or_else(|| invalid("Subkey binding was not found."))?;
734 let mut config = renew_signature(primary, previous)?;
735 refresh_config(&mut config, public.created_at(), payload)?;
736 let sig = config
737 .sign_subkey_binding(primary, primary.public_key(), password, &public)
738 .map_err(|_| invalid("Subkey expiration update failed."))?;
739 signatures.push(sig);
740 } else {
741 if primary.version() == KeyVersion::V6 {
742 let previous = secret
743 .details
744 .direct_signatures
745 .iter()
746 .filter(|s| {
747 s.typ() == Some(SignatureType::Key)
748 && s.verify_key(primary.public_key()).is_ok()
749 })
750 .max_by_key(|s| s.created())
751 .ok_or_else(|| invalid("Self signature was not found."))?;
752 let mut config = renew_signature(primary, previous)?;
753 refresh_config(&mut config, primary.created_at(), payload)?;
754 secret.details.direct_signatures.push(
755 config
756 .sign_key(primary, password, primary.public_key())
757 .map_err(|_| invalid("Expiration update failed."))?,
758 );
759 } else {
760 if let Some(previous) = secret
761 .details
762 .direct_signatures
763 .iter()
764 .filter(|s| {
765 s.typ() == Some(SignatureType::Key)
766 && s.verify_key(primary.public_key()).is_ok()
767 })
768 .max_by_key(|s| s.created())
769 {
770 let mut config = renew_signature(primary, previous)?;
771 refresh_config(&mut config, primary.created_at(), payload)?;
772 secret.details.direct_signatures.push(
773 config
774 .sign_key(primary, password, primary.public_key())
775 .map_err(|_| invalid("Expiration update failed."))?,
776 );
777 }
778 for user in &mut secret.details.users {
779 let previous = user
780 .signatures
781 .iter()
782 .filter(|s| {
783 matches!(
784 s.typ(),
785 Some(
786 SignatureType::CertGeneric
787 | SignatureType::CertPersona
788 | SignatureType::CertCasual
789 | SignatureType::CertPositive
790 )
791 ) && s
792 .verify_certification(
793 primary.public_key(),
794 pgp::types::Tag::UserId,
795 &user.id,
796 )
797 .is_ok()
798 })
799 .max_by_key(|s| s.created())
800 .ok_or_else(|| invalid("Self certification was not found."))?;
801 let mut config = renew_signature(primary, previous)?;
802 refresh_config(&mut config, primary.created_at(), payload)?;
803 user.signatures.push(
804 config
805 .sign_certification(
806 primary,
807 primary.public_key(),
808 password,
809 pgp::types::Tag::UserId,
810 &user.id,
811 )
812 .map_err(|_| invalid("Expiration update failed."))?,
813 );
814 }
815 }
816 }
817 Ok(())
819fn revocation_certificate(secret: &SignedSecretKey, password: &Password) -> CoreResult<String> {
820 let mut public = SignedPublicKey::from(secret.clone());
821 let sig = signature_config(&secret.primary_key, SignatureType::KeyRevocation)?
822 .sign_key(&secret.primary_key, password, &public.primary_key)
823 .map_err(|_| invalid("Revocation generation failed."))?;
824 public.details.revocation_signatures.push(sig);
825 public
826 .to_armored_string(Default::default())
827 .map_err(|_| invalid("Revocation export failed."))
829/// Revocation certificate (the public certificate with a key-revocation signature) for a stored
830/// V4/V6 identity, without changing the stored key. Anyone holding it can revoke the key.
831pub fn revocation_certificate_for(entry: &Value, passphrase: &str) -> CoreResult<String> {
832 if entry["kind"] != "openpgp" {
833 return Err(invalid("Choose an OpenPGP private identity."));
834 }
835 if super::openpgp_compat::handles(entry["public"].as_str().unwrap_or("").as_bytes())? {
836 return Err(invalid("Revocation certificates are available for V4 and V6 keys."));
837 }
838 if passphrase.chars().count() > 1024 || passphrase.contains('\0') {
839 return Err(invalid("Invalid key passphrase."));
840 }
841 let (secret, _) = SignedSecretKey::from_reader_single(std::io::Cursor::new(
842 entry["private"].as_str().ok_or_else(|| invalid("Private key is unavailable."))?,
843 ))
844 .map_err(|_| invalid("Invalid private key."))?;
845 secret.verify_bindings().map_err(|_| invalid("Invalid certificate bindings."))?;
846 revocation_certificate(&secret, &Password::from(passphrase)).map_err(|_| invalid("Check the key passphrase."))
848pub fn artifacts(entry: &Value, id: &str, revocation: &str) -> Value {
849 let base = file_base(
850 entry["label"].as_str().unwrap_or(""),
851 entry["fingerprint"].as_str().unwrap_or(""),
852 );
853 json!({"generationId":id,"family":"openpgp","identity":crate::core::keychain::describe(entry),"files":[
854 {"name":format!("{base} – Soukromé.asc"),"kind":"private","sensitive":true,"content":entry["private"]},
855 {"name":format!("{base} – Veřejné.asc"),"kind":"public","sensitive":false,"content":entry["public"]},
856 {"name":format!("{base} – Revokace.rev"),"kind":"revocation","sensitive":true,"content":revocation}
857 ]})
859/// Mutate a certificate without replacing its identity or re-encrypting existing packets.
860/// The decrypted primary is a short-lived zeroizing clone; the original protected packet is restored.
861pub fn edit(
862 entry: &Value,
863 payload: &Value,
864 progress: &dyn Fn(&str) -> CoreResult<()>,
865) -> CoreResult<Value> {
866 if entry["kind"] != "openpgp" {
867 return Err(invalid("Choose an OpenPGP private identity."));
868 }
869 if super::openpgp_compat::handles(entry["public"].as_str().unwrap_or("").as_bytes())? {
870 return super::openpgp_compat::edit(entry,payload,progress);
871 }
872 let metadata = describe_public(entry["public"].as_str().unwrap_or(""))?;
873 if metadata["revoked"] == true || ![4, 6].contains(&metadata["version"].as_u64().unwrap_or(0)) {
874 return Err(invalid("This certificate cannot be edited."));
875 }
876 let (mut secret, _) = SignedSecretKey::from_reader_single(std::io::Cursor::new(
877 entry["private"]
878 .as_str()
879 .ok_or_else(|| invalid("Private key is unavailable."))?,
880 ))
881 .map_err(|_| invalid("Invalid private key."))?;
882 secret
883 .verify_bindings()
884 .map_err(|_| invalid("Invalid certificate bindings."))?;
885 let protected = secret.primary_key.clone();
886 let passphrase = payload["passphrase"].as_str().unwrap_or("");
887 if passphrase.chars().count() > 1024 || passphrase.contains('\0') {
888 return Err(invalid("Invalid key passphrase."));
889 }
890 let password = Password::from(passphrase);
891 progress("unlock")?;
892 secret
893 .primary_key
894 .remove_password(&password)
895 .map_err(|_| invalid("Check the key passphrase."))?;
896 let target = payload["fingerprint"].as_str().filter(|s| !s.is_empty());
897 match payload["operation"].as_str().unwrap_or("") {
898 "addSubkey" => {
899 if metadata["expired"] == true {
900 return Err(invalid("Renew the primary key before adding subkeys."));
901 }
902 if secret.secret_subkeys.len() + secret.public_subkeys.len() >= 64 {
903 return Err(invalid("Too many subkeys."));
904 }
905 progress("subkey")?;
906 let mut sub = make_subkey(&secret.primary_key, &payload["subkey"])?;
907 progress("protect")?;
908 let new_password = payload["subkeyPassphrase"]
909 .as_str()
910 .unwrap_or(payload["passphrase"].as_str().unwrap_or(""));
911 if new_password.chars().count() > 1024 || new_password.contains('\0') {
912 return Err(invalid("Invalid subkey passphrase."));
913 }
914 if !new_password.is_empty() {
915 sub.key
916 .set_password(&mut rand::thread_rng(), &Password::from(new_password))
917 .map_err(|_| invalid("Subkey protection failed."))?;
918 }
919 secret.secret_subkeys.push(sub);
920 }
921 "revokeUserId" => {
922 if payload["confirmed"]!=true{return Err(invalid("Explicit user ID revocation confirmation is required."))}
923 let uid=payload["userId"].as_str().ok_or_else(||invalid("Choose a user ID."))?;
924 let user=secret.details.users.iter_mut().find(|u|u.id.id()==uid.as_bytes()).ok_or_else(||invalid("User ID was not found."))?;
925 let sig=signature_config(&secret.primary_key,SignatureType::CertRevocation)?
926 .sign_certification(&secret.primary_key,secret.primary_key.public_key(),&Password::empty(),pgp::types::Tag::UserId,&user.id)
927 .map_err(|_|invalid("User ID revocation failed."))?;
928 user.signatures.push(sig);
929 }
930 "revokeSubkey" => {
931 if payload["confirmed"] != true {
932 return Err(invalid(
933 "Explicit subkey revocation confirmation is required.",
934 ));
935 }
936 let fp = target.ok_or_else(|| invalid("Choose a subkey fingerprint."))?;
937 // Public-only subkeys can also be revoked by the private primary.
938 let subkey = secret
939 .secret_subkeys
940 .iter()
941 .find(|s| hex::encode_upper(s.key.fingerprint().as_ref()) == fp)
942 .map(|s| s.key.public_key().clone())
943 .or_else(|| {
944 secret
945 .public_subkeys
946 .iter()
947 .find(|s| hex::encode_upper(s.key.fingerprint().as_ref()) == fp)
948 .map(|s| s.key.clone())
949 })
950 .ok_or_else(|| invalid("Subkey was not found."))?;
951 let sig = signature_config(&secret.primary_key, SignatureType::SubkeyRevocation)?
952 .sign_subkey_binding(
953 &secret.primary_key,
954 secret.primary_key.public_key(),
955 &Password::empty(),
956 &subkey,
957 )
958 .map_err(|_| invalid("Subkey revocation failed."))?;
959 for sub in &mut secret.secret_subkeys {
960 if sub.key.fingerprint() == subkey.fingerprint() {
961 sub.signatures.push(sig.clone());
962 }
963 }
964 for sub in &mut secret.public_subkeys {
965 if sub.key.fingerprint() == subkey.fingerprint() {
966 sub.signatures.push(sig.clone());
967 }
968 }
969 }
970 "setExpiration" => {
971 if payload.get("expiresAt").is_none() {
972 return Err(invalid("Choose a key expiration."));
973 }
974 apply_expiration(&mut secret, &Password::empty(), target, payload)?;
975 }
976 _ => return Err(invalid("Unknown key operation.")),
977 }
978 progress("verify")?;
979 secret.primary_key = protected;
980 secret
981 .verify_bindings()
982 .map_err(|_| invalid("Updated certificate verification failed."))?;
983 let mut next = entry.clone();
984 next["private"] = json!(secret
985 .to_armored_string(Default::default())
986 .map_err(|_| invalid("Private export failed."))?);
987 next["public"] = json!(SignedPublicKey::from(secret)
988 .to_armored_string(Default::default())
989 .map_err(|_| invalid("Public export failed."))?);
990 // A previous successful upload does not attest to this new certificate revision.
991 next.as_object_mut().unwrap().remove("keyserver");
992 describe_public(next["public"].as_str().unwrap_or(""))?;
993 Ok(next)
996#[cfg(test)]
997mod tests {
998 use super::*;
999 #[test]
1000 #[ignore = "explicit local performance measurement; synthetic keys only"]
1001 fn generation_timing() {
1002 let algorithm =
1003 std::env::var("CHJ_PGP_BENCH_ALGORITHM").unwrap_or_else(|_| "ed25519".into());
1004 let start = std::time::Instant::now();
1005 let (output,_)=generate(&json!({"name":"Synthetic timing fixture","version":6,"algorithm":algorithm,"passphrase":"synthetic-timing-password","subkeys":[{"role":"encrypt","algorithm":"curve25519"},{"role":"sign","algorithm":"ed25519"},{"role":"authenticate","algorithm":"ed25519"}]})).unwrap();
1006 assert_eq!(output["files"].as_array().unwrap().len(), 3);
1007 eprintln!(
1008 "OpenPGP generation {} elapsed_ms={}",
1009 algorithm,
1010 start.elapsed().as_millis()
1011 );
1013 #[test]
1014 fn modern_lifecycle_preserves_identity_passwords_and_signed_expirations() {
1015 use crate::core::keychain;
1016 use pgp::ser::Serialize;
1017 let root = tempfile::tempdir().unwrap();
1018 let path = root.path().join("payload.txt");
1019 std::fs::write(&path, b"synthetic lifecycle data").unwrap();
1020 for version in [4, 6] {
1021 let expires = Timestamp::now().as_secs() as u64 + 86400;
1022 let (_,entry)=generate(&json!({"name":"Lifecycle fixture","version":version,"primaryCapabilities":"C","passphrase":"fixture-password","expiresAt":expires,"subkeys":[]})).unwrap();
1023 assert_eq!(keychain::describe(&entry)["expiresAt"], expires);
1024 let public_before = entry["public"].clone();
1025 assert!(edit(&entry,&json!({"operation":"addSubkey","passphrase":"wrong","subkey":{"role":"sign","algorithm":"ed25519"}}),&|_|Ok(())).is_err());
1026 assert_eq!(entry["public"], public_before);
1027 if version==4 || version==6 {
1028 assert!(revocation_certificate_for(&entry,"wrong").is_err());
1029 let rev=revocation_certificate_for(&entry,"fixture-password").unwrap();
1030 assert_eq!(describe_public(&rev).unwrap()["revoked"],true);
1031 assert_eq!(describe_public(entry["public"].as_str().unwrap()).unwrap()["revoked"],false);
1033 let added=edit(&entry,&json!({"operation":"addSubkey","passphrase":"fixture-password","subkey":{"role":"sign","algorithm":"ed25519","expiresAt":expires}}),&|_|Ok(())).unwrap();
1034 assert_eq!(entry["id"], added["id"]);
1035 assert_eq!(entry["fingerprint"], added["fingerprint"]);
1036 let (initial, _) = SignedSecretKey::from_reader_single(std::io::Cursor::new(
1037 entry["private"].as_str().unwrap(),
1038 ))
1039 .unwrap();
1040 let (secret, _) = SignedSecretKey::from_reader_single(std::io::Cursor::new(
1041 added["private"].as_str().unwrap(),
1042 ))
1043 .unwrap();
1044 assert_eq!(
1045 initial.primary_key.to_bytes().unwrap(),
1046 secret.primary_key.to_bytes().unwrap(),
1047 "existing encrypted primary packet changed"
1048 );
1049 assert!(secret.secret_subkeys[0].key.secret_params().is_encrypted());
1050 let meta = keychain::describe(&added);
1051 let fp = meta["subkeys"][0]["fingerprint"].clone();
1052 assert_eq!(meta["subkeys"][0]["expiresAt"], expires);
1053 assert_eq!(meta["certificateCapabilities"], "CS");
1054 let signature = keychain::sign(&added, &path, "fixture-password").unwrap();
1055 assert!(keychain::verify(&added, &path, signature.as_bytes()).is_ok());
1056 let renewed=edit(&added,&json!({"operation":"setExpiration","passphrase":"fixture-password","expiresAt":expires+86400}),&|_|Ok(())).unwrap();
1057 assert_eq!(keychain::describe(&renewed)["expiresAt"], expires + 86400);
1058 let renewed=edit(&renewed,&json!({"operation":"setExpiration","passphrase":"fixture-password","fingerprint":fp,"expiresAt":null}),&|_|Ok(())).unwrap();
1059 let meta = keychain::describe(&renewed);
1060 assert!(meta["subkeys"][0]["expiresAt"].is_null());
1061 assert_eq!(meta["subkeys"][0]["effectiveCapabilities"], "S");
1062 assert!(edit(&renewed,&json!({"operation":"revokeSubkey","passphrase":"fixture-password","fingerprint":fp}),&|_|Ok(())).is_err());
1063 let revoked=edit(&renewed,&json!({"operation":"revokeSubkey","confirmed":true,"passphrase":"fixture-password","fingerprint":fp}),&|_|Ok(())).unwrap();
1064 let meta = keychain::describe(&revoked);
1065 assert_eq!(meta["revoked"], false);
1066 assert_eq!(meta["subkeys"][0]["revoked"], true);
1067 assert_eq!(meta["certificateCapabilities"], "C");
1068 assert!(keychain::sign(&revoked, &path, "fixture-password").is_err());
1069 assert!(keychain::verify(&revoked, &path, signature.as_bytes()).is_err());
1070 assert!(edit(&revoked,&json!({"operation":"setExpiration","passphrase":"fixture-password","fingerprint":fp,"expiresAt":null}),&|_|Ok(())).is_err());
1071 let fully_revoked = keychain::revoke(&revoked, "fixture-password").unwrap();
1072 assert!(edit(&fully_revoked,&json!({"operation":"addSubkey","passphrase":"fixture-password","subkey":{"role":"sign"}}),&|_|Ok(())).is_err());
1075 #[test]
1076 fn generation_progress_is_real_cancellable_and_defaults_to_v6() {
1077 let phases = std::cell::RefCell::new(Vec::new());
1078 let (_, entry) = generate_progress(&json!({"name":"Progress fixture"}), &|phase| {
1079 phases.borrow_mut().push(phase.to_string());
1080 Ok(())
1081 })
1082 .unwrap();
1083 assert_eq!(keychain_metadata_version(&entry), 6);
1084 assert_eq!(
1085 *phases.borrow(),
1086 vec!["primary", "subkey", "revocation", "protect", "verify"]
1087 );
1088 assert!(
1089 generate_progress(&json!({"name":"Cancelled fixture"}), &|_| Err(invalid(
1090 "cancelled"
1091 )))
1092 .is_err()
1093 );
1094 assert!(generate(&json!({"name":"Bad expiration","expiresAt":1})).is_err());
1096 fn keychain_metadata_version(entry: &Value) -> u64 {
1097 crate::core::keychain::describe(entry)["version"]
1098 .as_u64()
1099 .unwrap()
1101 #[test]
1102 fn generation_protects_primary_and_every_subkey_and_exports_all_certificates() {
1103 let payload = json!({"name":"Synthetic identity","email":"fixture@example.invalid","passphrase":"synthetic-key-passphrase","subkeys":[{"role":"encrypt","algorithm":"curve25519"},{"role":"sign","algorithm":"ed25519"},{"role":"authenticate","algorithm":"ed25519"}]});
1104 let (output, entry) = generate(&payload).unwrap();
1105 assert_eq!(output["files"].as_array().unwrap().len(), 3);
1106 let (secret, _) = pgp::composed::SignedSecretKey::from_reader_single(std::io::Cursor::new(
1107 entry["private"].as_str().unwrap(),
1108 ))
1109 .unwrap();
1110 assert!(secret.primary_key.secret_params().is_encrypted());
1111 assert_eq!(secret.secret_subkeys.len(), 3);
1112 assert!(secret
1113 .secret_subkeys
1114 .iter()
1115 .all(|s| s.key.secret_params().is_encrypted()));
1116 let details = describe_public(entry["public"].as_str().unwrap()).unwrap();
1117 assert!(details["userIds"][0]
1118 .as_str()
1119 .unwrap()
1120 .contains("fixture@example.invalid"));
1121 assert_eq!(details["subkeys"].as_array().unwrap().len(), 3);
1122 let dir = tempfile::tempdir().unwrap();
1123 let path = dir.path().join("document");
1124 std::fs::write(&path, "fixture content").unwrap();
1125 assert!(crate::core::keychain::sign(&entry, &path, "wrong-password").is_err());
1126 let signature =
1127 crate::core::keychain::sign(&entry, &path, "synthetic-key-passphrase").unwrap();
1128 assert_eq!(
1129 crate::core::keychain::verify(&entry, &path, signature.as_bytes()).unwrap()["verified"],
1130 true
1131 );
1132 crate::core::keys::save_bundle(dir.path(), &output).unwrap();
1133 assert!(crate::core::keys::save_bundle(dir.path(), &output).is_err());
1134 for file in output["files"].as_array().unwrap() {
1135 assert_eq!(
1136 std::fs::read_to_string(dir.path().join(file["name"].as_str().unwrap())).unwrap(),
1137 file["content"].as_str().unwrap()
1138 );
1139 assert!(file["name"]
1140 .as_str()
1141 .unwrap()
1142 .ends_with(if file["kind"] == "revocation" {
1143 ".rev"
1144 } else {
1145 ".asc"
1146 }));
1148 let revoked = crate::core::keychain::parse(
1149 output["files"][2]["content"].as_str().unwrap().as_bytes(),
1150 "revoked",
1151 "",
1153 .unwrap();
1154 assert_eq!(revoked["revoked"], true);
1156 #[test]
1157 fn invalid_identities_and_subkey_capabilities_are_rejected() {
1158 assert!(generate(&json!({"name":"Name","email":"invalid"})).is_err());
1159 assert!(generate(&json!({"name":"Name\nOther"})).is_err());
1160 assert!(generate(
1161 &json!({"name":"Name","subkeys":[{"role":"encrypt","algorithm":"ed25519"}]})
1163 .is_err());
1164 assert!(
1165 generate(&json!({"name":"Name","subkeys":vec![json!({"role":"sign"});9]})).is_err()
1166 );
1168 #[test]
1169 fn versions_and_effective_capabilities_control_signing() {
1170 let dir = tempfile::tempdir().unwrap();
1171 let path = dir.path().join("data");
1172 std::fs::write(&path, "synthetic document").unwrap();
1173 for version in [4, 6] {
1174 let (_,entry)=generate(&json!({"name":"Capability fixture","version":version,"primaryCapabilities":"C","passphrase":"synthetic-password","subkeys":[{"role":"sign","capabilities":"SA","algorithm":"ed25519"},{"role":"encrypt","algorithm":"curve25519"}]})).unwrap();
1175 let meta = crate::core::keychain::describe(&entry);
1176 assert_eq!(meta["version"], version);
1177 assert_eq!(meta["declaredCapabilities"], "C");
1178 assert_eq!(meta["effectiveCapabilities"], "C");
1179 assert_eq!(meta["derivedCapabilities"], "");
1180 assert_eq!(meta["certificateCapabilities"], "CSAE");
1181 assert_eq!(meta["subkeys"][0]["effectiveCapabilities"], "SA");
1182 assert_eq!(meta["subkeys"][0]["version"], version);
1183 let signature =
1184 crate::core::keychain::sign(&entry, &path, "synthetic-password").unwrap();
1185 assert_eq!(
1186 crate::core::keychain::verify(&entry, &path, signature.as_bytes()).unwrap()
1187 ["verified"],
1188 true
1189 );
1190 let (detached, _) = pgp::composed::DetachedSignature::from_armor_single(
1191 std::io::Cursor::new(signature.as_bytes()),
1193 .unwrap();
1194 assert!(detached
1195 .signature
1196 .issuer_fingerprint()
1197 .iter()
1198 .any(|fp| hex::encode_upper(fp.as_ref()) == meta["subkeys"][0]["fingerprint"]));
1199 let revoked = crate::core::keychain::revoke(&entry, "synthetic-password").unwrap();
1200 let meta = crate::core::keychain::describe(&revoked);
1201 assert_eq!(meta["effectiveCapabilities"], "");
1202 assert_eq!(meta["certificateCapabilities"], "");
1203 assert_eq!(meta["signSupported"], false);
1204 assert!(crate::core::keychain::verify(&revoked, &path, signature.as_bytes()).is_err());
1206 let (_, entry) =
1207 generate(&json!({"name":"Certification only","primaryCapabilities":"C","subkeys":[]}))
1208 .unwrap();
1209 assert!(crate::core::keychain::sign(&entry, &path, "").is_err());
1210 // A mathematical signature from a C-only primary is still disallowed by its certified intent.
1211 let (secret, _) = pgp::composed::SignedSecretKey::from_reader_single(std::io::Cursor::new(
1212 entry["private"].as_str().unwrap(),
1213 ))
1214 .unwrap();
1215 let signature = pgp::composed::DetachedSignature::sign_binary_data(
1216 rand::thread_rng(),
1217 &secret.primary_key,
1218 &pgp::types::Password::empty(),
1219 pgp::crypto::hash::HashAlgorithm::Sha256,
1220 std::fs::File::open(&path).unwrap(),
1222 .unwrap()
1223 .to_armored_string(Default::default())
1224 .unwrap();
1225 assert!(crate::core::keychain::verify(&entry, &path, signature.as_bytes()).is_err());
1226 for version in [2, 3, 7] {
1227 assert!(generate(&json!({"name":"Unsupported preset","version":version})).is_err());
1229 assert_eq!(capabilities_from_bits(0x2f), "CSAE");
1230 assert_eq!(capabilities_from_bits(4), "E");
1231 assert_eq!(capabilities_from_bits(8), "E");
1232 assert!(generate(&json!({"name":"Invalid","primaryCapabilities":"CE"})).is_err());
1233 assert!(generate(&json!({"name":"Invalid","subkeys":[{"role":"sign","algorithm":"ed25519","capabilities":"E"}]})).is_err());
1234 assert!(generate(&json!({"name":"Invalid","subkeys":[{"role":"encrypt","algorithm":"curve25519","capabilities":"SA"}]})).is_err());
1236 #[test]
1237 fn unbound_components_cannot_contribute_effective_capabilities() {
1238 let (_,entry)=generate(&json!({"name":"Binding fixture","primaryCapabilities":"C","subkeys":[{"role":"sign","algorithm":"ed25519"},{"role":"encrypt","algorithm":"curve25519"}]})).unwrap();
1239 let (mut key, _) = SignedPublicKey::from_reader_single(std::io::Cursor::new(
1240 entry["public"].as_str().unwrap(),
1241 ))
1242 .unwrap();
1243 assert_eq!(
1244 describe_public(&key.to_armored_string(Default::default()).unwrap()).unwrap()
1245 ["certificateCapabilities"],
1246 "CSE"
1247 );
1248 key.public_subkeys[0].signatures.clear();
1249 let meta = describe_public(&key.to_armored_string(Default::default()).unwrap()).unwrap();
1250 assert!(!meta["certificateCapabilities"]
1251 .as_str()
1252 .unwrap()
1253 .contains('S'));
1254 for user in &mut key.details.users {
1255 user.signatures.clear();
1257 key.details.direct_signatures.clear();
1258 let meta = describe_public(&key.to_armored_string(Default::default()).unwrap()).unwrap();
1259 assert_eq!(meta["certificateCapabilities"], "");
1260 assert_eq!(meta["effectiveCapabilities"], "");
1262 #[test]
1263 #[ignore = "requires local GnuPG; all keys and homes are temporary synthetic fixtures"]
1264 fn generated_armored_certificates_and_subkeys_interoperate_with_gnupg() {
1265 use std::io::Write;
1266 use std::process::{Command, Stdio};
1267 let root = tempfile::tempdir().unwrap();
1268 let home = root.path().join("gnupg");
1269 crate::core::storage::ensure_private_dir(&home).unwrap();
1270 let (output,mut entry) = generate(&json!({"version":4,"name":"GnuPG fixture","email":"fixture@example.invalid","passphrase":"synthetic-gpg-password","subkeys":[{"role":"encrypt","algorithm":"curve25519"},{"role":"sign","algorithm":"ed25519"}]})).unwrap();
1271 let expires = Timestamp::now().as_secs() as u64 + 86400 * 30;
1272 entry=edit(&entry,&json!({"operation":"addSubkey","passphrase":"synthetic-gpg-password","subkey":{"role":"authenticate","algorithm":"ed25519"}}),&|_|Ok(())).unwrap();
1273 let fp = crate::core::keychain::describe(&entry)["subkeys"][2]["fingerprint"].clone();
1274 entry=edit(&entry,&json!({"operation":"setExpiration","passphrase":"synthetic-gpg-password","expiresAt":expires}),&|_|Ok(())).unwrap();
1275 entry=edit(&entry,&json!({"operation":"revokeSubkey","passphrase":"synthetic-gpg-password","fingerprint":fp,"confirmed":true}),&|_|Ok(())).unwrap();
1276 let output = artifacts(
1277 &entry,
1278 output["generationId"].as_str().unwrap(),
1279 output["files"][2]["content"].as_str().unwrap(),
1280 );
1281 crate::core::keys::save_bundle(root.path(), &output).unwrap();
1282 let command = || {
1283 let mut c = Command::new("gpg");
1284 c.env("GNUPGHOME", &home).args(["--no-options", "--batch"]);
1286 };
1287 let imported = command()
1288 .arg("--import")
1289 .arg(
1290 root.path()
1291 .join(output["files"][0]["name"].as_str().unwrap()),
1293 .output()
1294 .expect("GnuPG must be installed for this explicit test");
1295 assert!(
1296 imported.status.success(),
1297 "GnuPG import of protected secret certificate failed"
1298 );
1299 let listing = command()
1300 .args(["--with-colons", "--list-secret-keys"])
1301 .output()
1302 .unwrap();
1303 assert!(listing.status.success());
1304 let listing = String::from_utf8_lossy(&listing.stdout);
1305 assert!(
1306 listing.lines().any(|line| line.starts_with("sec:")
1307 && line.split(':').nth(6) == Some(expires.to_string().as_str())),
1308 "GnuPG did not recognize updated expiration"
1309 );
1310 assert!(
1311 listing.lines().any(|line| line.starts_with("ssb:r:")),
1312 "GnuPG did not recognize subkey revocation"
1313 );
1314 let data = root.path().join("document");
1315 let sig = root.path().join("document.asc");
1316 std::fs::write(&data, "synthetic content").unwrap();
1317 let mut child = command()
1318 .args([
1319 "--pinentry-mode",
1320 "loopback",
1321 "--passphrase-fd",
1322 "0",
1323 "--armor",
1324 "--detach-sign",
1325 "--local-user",
1326 entry["fingerprint"].as_str().unwrap(),
1327 "--output",
1328 ])
1329 .arg(&sig)
1330 .arg(&data)
1331 .stdin(Stdio::piped())
1332 .stdout(Stdio::piped())
1333 .stderr(Stdio::piped())
1334 .spawn()
1335 .unwrap();
1336 child
1337 .stdin
1338 .take()
1339 .unwrap()
1340 .write_all(b"synthetic-gpg-password\n")
1341 .unwrap();
1342 assert!(
1343 child.wait_with_output().unwrap().status.success(),
1344 "GnuPG could not sign using the protected generated subkey"
1345 );
1346 assert!(crate::core::keychain::verify(&entry, &data, &std::fs::read(sig).unwrap()).is_ok());
1347 let revocation_import = command()
1348 .arg("--import")
1349 .arg(
1350 root.path()
1351 .join(output["files"][2]["name"].as_str().unwrap()),
1353 .output()
1354 .unwrap();
1355 assert!(
1356 revocation_import.status.success(),
1357 "GnuPG rejected the synthetic public revocation certificate: {}",
1358 String::from_utf8_lossy(&revocation_import.stderr)
1359 );
1360 let listing = command()
1361 .args(["--with-colons", "--list-keys"])
1362 .output()
1363 .unwrap();
1364 assert!(
1365 String::from_utf8_lossy(&listing.stdout)
1366 .lines()
1367 .any(|line| line.starts_with("pub:r:")),
1368 "GnuPG did not recognize the generated revocation certificate"
1369 );
1371 #[test]
1372 #[ignore = "requires the explicitly supplied local demonstration key; no private material is printed or copied"]
1373 fn supplied_demonstration_certificate_imports_with_its_subkeys() {
1374 let path =
1375 std::env::var("CHJ_DEMO_OPENPGP_KEY").expect("provide the demonstration key path");
1376 let bytes = Zeroizing::new(std::fs::read(path).unwrap());
1377 let entry = crate::core::keychain::parse(&bytes, "", "").unwrap();
1378 let summary = crate::core::keychain::describe(&entry);
1379 assert_eq!(summary["kind"], "openpgp");
1380 assert_eq!(summary["hasPrivate"], true);
1381 assert!(summary["subkeys"].as_array().unwrap().len() >= 4);
1382 assert!(summary.get("private").is_none());
SHA-25690448bd92f3ab5fb489c8ca225a28b2b113fd6f91abff77180d0cbebf50ff3d1

CH-J Proprietary Software License 1.14 · 源代码依据 CH-J Proprietary Software License 1.14 提供。公开源代码不会改变许可条款,也不会授予额外权利。

压缩包 SHA-256ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f
文件