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