Veröffentlichte Quellen
Quellen als ZIP herunterladen
CH-J Tools
Durchsuchen Sie Verzeichnisse und Dateien einer bestimmten Anwendungsausgabe.
CH-J / Server Manager
1704 Dateien
0.1.2-beta
Hinzugefügt: · Erstellt:
1
//! Local OpenPGP generation and certificate descriptions. No external GnuPG/home/keyserver.2
use crate::core::error::{CoreError, CoreResult};3
use 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
};12
use serde_json::{json, Value};13
use zeroize::Zeroizing;14
fn invalid(message: &str) -> CoreError {15
CoreError::new("OPENPGP_INPUT_INVALID", message)16
}18
pub 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()23
}24
fn 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
)43
}44
fn flag_bits(signature: &pgp::packet::Signature) -> Option<u8> {45
signature46
.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
})58
}59
fn 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
None75
};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) <= now89
})90
} else {91
binding.is_some_and(|sig| !key_unexpired(created, sig, now))92
};93
let usable = binding_verified94
&& parent_usable95
&& !revoked96
&& !expired97
&& created <= now + std::time::Duration::from_secs(300)98
&& binding.is_some_and(|sig| signature_current(sig, now))99
&& version != 5100
&& !(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
binding113
.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
details120
}121
pub 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> = key136
.details137
.direct_signatures138
.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 = user145
.signatures146
.iter()147
.filter(|sig| {148
sig.typ() == Some(SignatureType::CertRevocation)149
&& sig150
.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::CertGeneric161
| SignatureType::CertPersona162
| SignatureType::CertCasual163
| SignatureType::CertPositive164
)165
) {166
continue;167
}168
if key.primary_key.version() != pgp::types::KeyVersion::V6169
&& sig170
.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_signatures182
.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 = key191
.details192
.revocation_signatures193
.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> = key205
.public_subkeys206
.iter()207
.map(|sub| {208
let binding = sub209
.signatures210
.iter()211
.filter(|sig| {212
sig.typ() == Some(SignatureType::SubkeyBinding)213
&& sig214
.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
&& sig222
.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"] == true241
&& 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
metadata255
})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> = key269
.details270
.users271
.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)283
}285
pub fn file_base(label: &str, fingerprint: &str) -> String {286
let label: String = label287
.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 = fingerprint294
.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
short306
)307
}308
fn 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."))}312
}313
fn 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::Ed25519321
} else {322
KeyType::Ed25519Legacy323
}),324
"curve25519" if encrypt => Ok(if version == pgp::types::KeyVersion::V6 {325
KeyType::X25519326
} 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
}342
}343
pub fn generate(payload: &Value) -> CoreResult<(Value, Value)> {344
generate_progress(payload, &|_| Ok(()))345
}346
fn 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) => value362
.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 = payload381
.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::All418
} else {419
EncryptionCaps::None420
})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
secret438
.primary_key439
.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.key444
.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
secret451
.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))458
}460
/// A progress callback contains phase names only, never identity or secret material.461
pub 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)484
}485
fn validate_identity(name: &str, email: &str, comment: &str) -> CoreResult<()> {486
if name.is_empty()487
|| name.chars().count() > 100488
|| 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() > 254494
|| email.matches('@').count() != 1495
|| email.starts_with('@')496
|| email.ends_with('@')497
|| email498
.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() > 100504
|| comment505
.chars()506
.any(|c| c.is_control() || "()<>".contains(c))507
{508
return Err(invalid("Invalid OpenPGP identity comment."));509
}510
Ok(())511
}512
fn packet(data: SubpacketData) -> CoreResult<Subpacket> {513
Subpacket::regular(data).map_err(|_| invalid("Invalid signature subpacket."))514
}515
fn 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)531
}532
fn 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![]))542
}543
fn 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 = value551
.as_u64()552
.ok_or_else(|| invalid("Invalid key expiration."))?;553
let now = Timestamp::now().as_secs() as u64;554
if at <= now555
|| at <= created.as_secs() as u64556
|| at - created.as_secs() as u64 > u32::MAX as u64557
{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
))?))565
}566
fn 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 = config573
.hashed_subpackets574
.iter()575
.filter_map(|p| {576
if let SubpacketData::SignatureCreationTime(t) = p.data {577
Some(t)578
} else {579
None580
}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
config594
.hashed_subpackets595
.push(packet(SubpacketData::SignatureCreationTime(created_at))?);596
if let Some(exp) = expiration_packet(created, payload)? {597
config.hashed_subpackets.push(exp);598
}599
Ok(())600
}601
fn 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)608
}609
fn 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
config654
.hashed_subpackets655
.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
config668
.hashed_subpackets669
.push(packet(SubpacketData::EmbeddedSignature(Box::new(back)))?);670
}671
let binding = config672
.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]))680
}681
fn renew_signature(682
primary: &pgp::packet::SecretKey,683
previous: &pgp::packet::Signature,684
) -> CoreResult<SignatureConfig> {685
let previous_config = previous686
.config()687
.ok_or_else(|| invalid("Invalid previous signature."))?;688
let mut config = signature_config(689
primary,690
previous691
.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)697
}698
fn 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) = secret710
.secret_subkeys711
.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 = secret717
.public_subkeys718
.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 signatures724
.iter()725
.any(|s| s.typ() == Some(SignatureType::SubkeyRevocation))726
{727
return Err(invalid("A revoked subkey cannot be renewed."));728
}729
let previous = signatures730
.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 = config737
.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 = secret743
.details744
.direct_signatures745
.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
config756
.sign_key(primary, password, primary.public_key())757
.map_err(|_| invalid("Expiration update failed."))?,758
);759
} else {760
if let Some(previous) = secret761
.details762
.direct_signatures763
.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
config774
.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 = user780
.signatures781
.iter()782
.filter(|s| {783
matches!(784
s.typ(),785
Some(786
SignatureType::CertGeneric787
| SignatureType::CertPersona788
| SignatureType::CertCasual789
| SignatureType::CertPositive790
)791
) && s792
.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
config805
.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(())818
}819
fn 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
public826
.to_armored_string(Default::default())827
.map_err(|_| invalid("Revocation export failed."))828
}829
/// Revocation certificate (the public certificate with a key-revocation signature) for a stored830
/// V4/V6 identity, without changing the stored key. Anyone holding it can revoke the key.831
pub 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."))847
}848
pub 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
]})858
}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.861
pub 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
secret883
.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
secret893
.primary_key894
.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.key916
.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 = secret939
.secret_subkeys940
.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
secret945
.public_subkeys946
.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
secret981
.verify_bindings()982
.map_err(|_| invalid("Updated certificate verification failed."))?;983
let mut next = entry.clone();984
next["private"] = json!(secret985
.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)994
}996
#[cfg(test)]997
mod 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
);1012
}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);1032
}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());1073
}1074
}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());1095
}1096
fn keychain_metadata_version(entry: &Value) -> u64 {1097
crate::core::keychain::describe(entry)["version"]1098
.as_u64()1099
.unwrap()1100
}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!(secret1113
.secret_subkeys1114
.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
true1131
);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
}));1147
}1148
let revoked = crate::core::keychain::parse(1149
output["files"][2]["content"].as_str().unwrap().as_bytes(),1150
"revoked",1151
"",1152
)1153
.unwrap();1154
assert_eq!(revoked["revoked"], true);1155
}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"}]})1162
)1163
.is_err());1164
assert!(1165
generate(&json!({"name":"Name","subkeys":vec![json!({"role":"sign"});9]})).is_err()1166
);1167
}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
true1189
);1190
let (detached, _) = pgp::composed::DetachedSignature::from_armor_single(1191
std::io::Cursor::new(signature.as_bytes()),1192
)1193
.unwrap();1194
assert!(detached1195
.signature1196
.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());1205
}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(),1221
)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());1228
}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());1235
}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();1256
}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"], "");1261
}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"]);1285
c1286
};1287
let imported = command()1288
.arg("--import")1289
.arg(1290
root.path()1291
.join(output["files"][0]["name"].as_str().unwrap()),1292
)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
child1337
.stdin1338
.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()),1352
)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
);1370
}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());1383
}1384
}SHA-256
90448bd92f3ab5fb489c8ca225a28b2b113fd6f91abff77180d0cbebf50ff3d1CH-J Proprietary Software License 1.14 · Die Quellen werden unter der CH-J Proprietary Software License 1.14 bereitgestellt. Ihre Verfügbarkeit ändert die Lizenzbedingungen nicht und gewährt keine zusätzlichen Rechte.
SHA-256 des Archivs
ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f