已發布的原始碼
下載原始碼 ZIP
CH-J Tools
瀏覽特定應用程式版本的目錄與檔案。
CH-J / Server Manager
1704 個檔案
0.1.2-beta
新增時間: · 建置時間:
1
//! Verifies a local file against a detached OpenPGP signature with a key the user chose. Nothing is fetched from2
//! the network. A fingerprint alone cannot verify a signature, so it either checks the supplied key or selects a3
//! key saved earlier. A valid result proves who signed with that key, not that the key can be trusted.5
use crate::core::error::{CoreError, CoreResult};6
use crate::core::updates::openpgp::{fingerprint_hex, key_unexpired, normalize_fingerprint, signature_current, system_time};7
use pgp::composed::{Deserializable, DetachedSignature, SignedPublicKey};8
use pgp::packet::{Signature, SignatureType};9
use pgp::types::KeyDetails;10
use serde_json::{json, Value};11
use std::io::Cursor;12
use std::path::Path;13
use std::time::SystemTime;15
pub const MAX_KEY_BYTES: u64 = 2 * 1024 * 1024;16
pub const MAX_SIGNATURE_BYTES: u64 = 1024 * 1024;18
fn fail(code: &str, message: &str) -> CoreError {19
CoreError::new(code, message)20
}22
fn armored(bytes: &[u8]) -> bool {23
String::from_utf8_lossy(&bytes[..bytes.len().min(4096)]).contains("-----BEGIN PGP")24
}26
fn parse_signature(bytes: &[u8]) -> CoreResult<Signature> {27
let malformed = || fail("HASH_PGP_SIGNATURE_MALFORMED", "The file is not a detached OpenPGP signature.");28
let detached = if armored(bytes) {29
DetachedSignature::from_armor_single(Cursor::new(bytes)).map_err(|_| malformed())?.030
} else {31
DetachedSignature::from_bytes(Cursor::new(bytes)).map_err(|_| malformed())?32
};33
if !covers_document(detached.signature.typ()) {34
return Err(malformed());35
}36
Ok(detached.signature)37
}39
/// Only binary and text signatures are made over a document. Standalone and timestamp signatures hash no file data,40
/// so one of them would "verify" any file.41
fn covers_document(kind: Option<SignatureType>) -> bool {42
matches!(kind, Some(SignatureType::Binary | SignatureType::Text))43
}45
/// A public key file may hold several keys, armored or binary.46
pub fn parse_keys(bytes: &[u8]) -> CoreResult<Vec<SignedPublicKey>> {47
let malformed = || fail("HASH_PGP_KEY_MALFORMED", "The file does not contain an OpenPGP public key.");48
let keys: Vec<SignedPublicKey> = if armored(bytes) {49
SignedPublicKey::from_armor_many(Cursor::new(bytes)).map_err(|_| malformed())?.0.filter_map(Result::ok).collect()50
} else {51
SignedPublicKey::from_bytes_many(Cursor::new(bytes)).map_err(|_| malformed())?.filter_map(Result::ok).collect()52
};53
if keys.is_empty() { Err(malformed()) } else { Ok(keys) }54
}56
fn user_ids(key: &SignedPublicKey) -> Vec<String> {57
key.details.users.iter().map(|user| String::from_utf8_lossy(user.id.id()).to_string()).collect()58
}60
fn describe(key: &SignedPublicKey) -> Value {61
json!({ "fingerprint": fingerprint_hex(&key.primary_key.fingerprint()), "userIds": user_ids(key) })62
}64
/// Issuer fingerprints of a detached signature, then its issuer key IDs (informational; the signature still has to verify).65
pub fn signature_issuers(bytes: &[u8]) -> Vec<String> {66
parse_signature(bytes).map(|signature| { let (fingerprints, ids) = issuers(&signature); fingerprints.into_iter().chain(ids).collect() }).unwrap_or_default()67
}69
fn issuers(signature: &Signature) -> (Vec<String>, Vec<String>) {70
(71
signature.issuer_fingerprint().iter().map(|fp| fingerprint_hex(fp)).collect(),72
signature.issuer_key_id().iter().map(|id| hex::encode_upper(id.as_ref())).collect(),73
)74
}76
/// Revoked or expired keys do not verify anything, even when the mathematics of the signature hold.77
fn primary_usable(key: &SignedPublicKey, now: SystemTime) -> bool {78
if key.verify_bindings().is_err() || key.details.revocation_signatures.iter().any(|revocation| revocation.verify_key(&key.primary_key).is_ok()) {79
return false;80
}81
let created = system_time(key.primary_key.created_at());82
key.details.users.iter().flat_map(|user| user.signatures.iter()).filter(|sig| signature_current(sig, now)).max_by_key(|sig| sig.created().map(system_time)).is_some_and(|sig| key_unexpired(created, sig, now))83
}85
fn subkey_usable(key: &SignedPublicKey, index: usize, now: SystemTime) -> bool {86
let subkey = &key.public_subkeys[index];87
let mut binding: Option<&Signature> = None;88
for candidate in &subkey.signatures {89
if candidate.verify_subkey_binding(&key.primary_key, &subkey.key).is_err() {90
continue;91
}92
match candidate.typ() {93
Some(SignatureType::SubkeyRevocation) => return false,94
Some(SignatureType::SubkeyBinding) if signature_current(candidate, now) && binding.is_none_or(|b| b.created().map(system_time) < candidate.created().map(system_time)) => binding = Some(candidate),95
_ => {}96
}97
}98
binding.is_some_and(|b| key_unexpired(system_time(subkey.key.created_at()), b, now) && b.key_flags().sign())99
}101
fn verify_with(key: &SignedPublicKey, signature: &Signature, file: &Path, now: SystemTime) -> Option<CoreResult<String>> {102
let (fingerprints, ids) = issuers(signature);103
let names = |fingerprint: String, id: String| fingerprints.contains(&fingerprint) || ids.iter().any(|value| value.eq_ignore_ascii_case(&id));104
let open = || std::fs::File::open(file).map(|f| std::io::BufReader::with_capacity(1 << 20, f)).map_err(|_| fail("HASH_PGP_FILE_UNREADABLE", "The file could not be read."));105
let invalid_key = || Err(fail("HASH_PGP_KEY_INVALID", "The key is revoked, expired, or has an invalid self-signature."));106
let invalid = || fail("HASH_PGP_SIGNATURE_INVALID", "The signature does not match this file. The file was changed or the signature belongs to another file.");107
if names(fingerprint_hex(&key.primary_key.fingerprint()), hex::encode_upper(key.primary_key.legacy_key_id().as_ref())) {108
if !primary_usable(key, now) {109
return Some(invalid_key());110
}111
return Some(open().and_then(|data| signature.verify(&key.primary_key, data).map_err(|_| invalid())).map(|_| fingerprint_hex(&key.primary_key.fingerprint())));112
}113
for (index, subkey) in key.public_subkeys.iter().enumerate() {114
if names(fingerprint_hex(&subkey.key.fingerprint()), hex::encode_upper(subkey.key.legacy_key_id().as_ref())) {115
if !primary_usable(key, now) || !subkey_usable(key, index, now) {116
return Some(invalid_key());117
}118
return Some(open().and_then(|data| signature.verify(&subkey.key, data).map_err(|_| invalid())).map(|_| fingerprint_hex(&subkey.key.fingerprint())));119
}120
}121
None122
}124
fn saved_keys(keyring: &Path) -> Vec<SignedPublicKey> {125
let Ok(entries) = std::fs::read_dir(keyring) else { return Vec::new() };126
entries.flatten().filter_map(|entry| std::fs::read(entry.path()).ok()).filter_map(|bytes| parse_keys(&bytes).ok()).flatten().collect()127
}129
pub fn list_keys(keyring: &Path) -> Value {130
let mut keys: Vec<Value> = saved_keys(keyring).iter().map(describe).collect();131
keys.sort_by(|a, b| a["fingerprint"].as_str().cmp(&b["fingerprint"].as_str()));132
json!({ "keys": keys })133
}135
pub fn remove_key(keyring: &Path, fingerprint: &str) -> CoreResult<Value> {136
let fingerprint = expected_fingerprint(fingerprint)?.ok_or_else(|| fail("HASH_PGP_FINGERPRINT_INVALID", "Enter the full fingerprint of the key (40 or 64 hexadecimal characters)."))?;137
match std::fs::remove_file(keyring.join(format!("{fingerprint}.asc"))) {138
Ok(()) => Ok(json!({ "removed": true })),139
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(json!({ "removed": false })),140
Err(_) => Err(fail("HASH_PGP_KEY_STORE_FAILED", "The saved key could not be removed.")),141
}142
}144
fn save_key(keyring: &Path, key: &SignedPublicKey) -> CoreResult<()> {145
let failed = || fail("HASH_PGP_KEY_STORE_FAILED", "The key could not be saved.");146
let armored = key.to_armored_bytes(Default::default()).map_err(|_| failed())?;147
std::fs::create_dir_all(keyring).map_err(|_| failed())?;148
std::fs::write(keyring.join(format!("{}.asc", fingerprint_hex(&key.primary_key.fingerprint()))), armored).map_err(|_| failed())149
}151
/// Short key IDs can be forged, so only complete fingerprints count.152
fn expected_fingerprint(value: &str) -> CoreResult<Option<String>> {153
let normalized = normalize_fingerprint(value);154
if value.trim().is_empty() {155
return Ok(None);156
}157
if ![40, 64].contains(&normalized.len()) || value.chars().any(|c| !c.is_ascii_hexdigit() && !c.is_whitespace() && c != ':') {158
return Err(fail("HASH_PGP_FINGERPRINT_INVALID", "Enter the full fingerprint of the key (40 or 64 hexadecimal characters)."));159
}160
Ok(Some(normalized))161
}163
/// `key` is the public key the user supplied now. Without it, a saved key is used, chosen by the signature's issuer.164
pub fn verify_file(file: &Path, signature: &[u8], key: Option<&[u8]>, keyring: &Path, fingerprint: &str, save: bool) -> CoreResult<Value> {165
verify_at(file, signature, key, keyring, fingerprint, save, SystemTime::now())166
}168
fn verify_at(file: &Path, signature: &[u8], key: Option<&[u8]>, keyring: &Path, fingerprint: &str, save: bool, now: SystemTime) -> CoreResult<Value> {169
let expected = expected_fingerprint(fingerprint)?;170
let signature = parse_signature(signature)?;171
let supplied = key.is_some();172
let keys = match key {173
Some(bytes) => parse_keys(bytes)?,174
None => saved_keys(keyring),175
};176
if !signature_current(&signature, now) {177
return Err(fail("HASH_PGP_SIGNATURE_INVALID", "The signature has expired or is dated in the future."));178
}179
let mut outcome = None;180
for key in &keys {181
if let Some(result) = verify_with(key, &signature, file, now) {182
outcome = Some((key, result));183
break;184
}185
}186
let Some((key, result)) = outcome else {187
let (fingerprints, ids) = issuers(&signature);188
let issuer = fingerprints.first().or(ids.first()).cloned().unwrap_or_default();189
let (code, message) = if supplied {190
("HASH_PGP_SIGNER_UNKNOWN", "This signature was not made with the supplied key.")191
} else {192
("HASH_PGP_KEY_REQUIRED", "A fingerprint alone cannot verify a signature. Add the signer's public key.")193
};194
return Err(fail(code, message).with("issuer", Value::from(issuer)));195
};196
let signer = result?;197
let primary = fingerprint_hex(&key.primary_key.fingerprint());198
if let Some(expected) = &expected {199
if *expected != primary && *expected != signer {200
return Err(fail("HASH_PGP_FINGERPRINT_MISMATCH", "The signature is valid, but it was made by a different key than the fingerprint you entered.").with("primaryFingerprint", Value::from(primary)));201
}202
}203
if save && supplied {204
save_key(keyring, key)?;205
}206
let created = signature.created().map(|time| chrono::DateTime::<chrono::Utc>::from(system_time(time)).to_rfc3339_opts(chrono::SecondsFormat::Secs, true));207
Ok(json!({208
"valid": true,209
"primaryFingerprint": primary,210
"signerFingerprint": signer,211
"userIds": user_ids(key),212
"createdAt": created,213
"keySource": if supplied { "supplied" } else { "saved" },214
"fingerprintMatched": expected.is_some(),215
"saved": save && supplied216
}))217
}219
#[cfg(test)]220
mod tests {221
use super::*;222
use std::path::PathBuf;224
// Fixtures come from real GnuPG, see tests/fixtures/pgp/README.md.225
fn fixture(name: &str) -> PathBuf {226
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/pgp").join(name)227
}228
fn read(name: &str) -> Vec<u8> {229
std::fs::read(fixture(name)).unwrap()230
}231
fn fingerprint() -> String {232
String::from_utf8(read("signer.fingerprint")).unwrap().trim().to_string()233
}234
fn check(signature: &str, key: Option<&str>, keyring: &Path, expected: &str, save: bool) -> CoreResult<Value> {235
verify_file(&fixture("data.txt"), &read(signature), key.map(read).as_deref(), keyring, expected, save)236
}238
#[test]239
fn armored_and_binary_signatures_verify_with_a_public_key() {240
let dir = tempfile::tempdir().unwrap();241
for signature in ["data.txt.asc", "data.txt.sig"] {242
for key in ["signer.asc", "signer.gpg"] {243
let result = check(signature, Some(key), dir.path(), "", false).unwrap();244
assert_eq!(result["valid"], true, "{signature} {key}");245
assert_eq!(result["primaryFingerprint"], fingerprint());246
assert_eq!(result["fingerprintMatched"], false);247
assert!(result["userIds"][0].as_str().unwrap().contains("CH-J Test"));248
}249
}250
}252
#[test]253
fn a_changed_file_fails() {254
let dir = tempfile::tempdir().unwrap();255
let changed = dir.path().join("changed.txt");256
std::fs::write(&changed, "changed").unwrap();257
let error = verify_file(&changed, &read("data.txt.asc"), Some(&read("signer.asc")), dir.path(), "", false).unwrap_err();258
assert_eq!(error.code, "HASH_PGP_SIGNATURE_INVALID");259
}261
#[test]262
fn another_key_is_not_the_signer() {263
let dir = tempfile::tempdir().unwrap();264
assert_eq!(check("data.txt.asc", Some("other.asc"), dir.path(), "", false).unwrap_err().code, "HASH_PGP_SIGNER_UNKNOWN");265
}267
#[test]268
fn the_fingerprint_must_match_the_key() {269
let dir = tempfile::tempdir().unwrap();270
let spaced = fingerprint().as_bytes().chunks(4).map(|c| String::from_utf8_lossy(c).to_lowercase()).collect::<Vec<_>>().join(" ");271
assert_eq!(check("data.txt.asc", Some("signer.asc"), dir.path(), &spaced, false).unwrap()["fingerprintMatched"], true);272
let other = String::from_utf8(read("other.fingerprint")).unwrap();273
assert_eq!(check("data.txt.asc", Some("signer.asc"), dir.path(), other.trim(), false).unwrap_err().code, "HASH_PGP_FINGERPRINT_MISMATCH");274
assert_eq!(check("data.txt.asc", Some("signer.asc"), dir.path(), "ABCD1234", false).unwrap_err().code, "HASH_PGP_FINGERPRINT_INVALID");275
}277
#[test]278
fn a_fingerprint_alone_needs_a_saved_key() {279
let dir = tempfile::tempdir().unwrap();280
let error = check("data.txt.asc", None, dir.path(), &fingerprint(), false).unwrap_err();281
assert_eq!(error.code, "HASH_PGP_KEY_REQUIRED");282
assert_eq!(check("data.txt.asc", Some("signer.asc"), dir.path(), "", true).unwrap()["saved"], true);283
let result = check("data.txt.asc", None, dir.path(), &fingerprint(), false).unwrap();284
assert_eq!(result["keySource"], "saved");285
assert_eq!(result["fingerprintMatched"], true);286
assert_eq!(list_keys(dir.path())["keys"].as_array().unwrap().len(), 1);287
assert_eq!(remove_key(dir.path(), &fingerprint()).unwrap()["removed"], true);288
assert_eq!(check("data.txt.asc", None, dir.path(), &fingerprint(), false).unwrap_err().code, "HASH_PGP_KEY_REQUIRED");289
}291
#[test]292
fn a_saved_key_does_not_vouch_for_another_fingerprint() {293
let dir = tempfile::tempdir().unwrap();294
check("data.txt.asc", Some("signer.asc"), dir.path(), "", true).unwrap();295
let other = String::from_utf8(read("other.fingerprint")).unwrap();296
assert_eq!(check("data.txt.asc", None, dir.path(), other.trim(), false).unwrap_err().code, "HASH_PGP_FINGERPRINT_MISMATCH");297
}299
#[test]300
fn only_document_signatures_count() {301
assert!(covers_document(Some(SignatureType::Binary)));302
assert!(covers_document(Some(SignatureType::Text)));303
for kind in [SignatureType::Standalone, SignatureType::Timestamp, SignatureType::CertGeneric, SignatureType::SubkeyBinding] {304
assert!(!covers_document(Some(kind)), "{kind:?}");305
}306
assert!(!covers_document(None));307
}309
#[test]310
fn malformed_inputs_are_named() {311
let dir = tempfile::tempdir().unwrap();312
assert_eq!(verify_file(&fixture("data.txt"), b"nonsense", None, dir.path(), "", false).unwrap_err().code, "HASH_PGP_SIGNATURE_MALFORMED");313
assert_eq!(verify_file(&fixture("data.txt"), &read("data.txt.asc"), Some(b"nonsense"), dir.path(), "", false).unwrap_err().code, "HASH_PGP_KEY_MALFORMED");314
}316
#[test]317
fn revoked_and_expired_keys_do_not_verify() {318
let dir = tempfile::tempdir().unwrap();319
assert_eq!(check("data.txt.asc", Some("signer-revoked.asc"), dir.path(), "", false).unwrap_err().code, "HASH_PGP_KEY_INVALID");320
let later = SystemTime::now() + std::time::Duration::from_secs(10 * 365 * 24 * 3600);321
let error = verify_at(&fixture("data.txt"), &read("expiring.txt.asc"), Some(&read("expiring.asc")), dir.path(), "", false, later).unwrap_err();322
assert_eq!(error.code, "HASH_PGP_KEY_INVALID");323
}324
}SHA-256
7b502417b6f373080f2f13d9d68f453becde89f76d0ced74ac82456caa25eca5CH-J Proprietary Software License 1.14 · 原始碼依 CH-J Proprietary Software License 1.14 提供。公開原始碼不會改變授權條款,也不會授予額外的權利。
壓縮檔 SHA-256
ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f