Nailathalang source
I-download ang source ZIP
CH-J Tools
I-browse ang mga direktoryo at file ng isang partikular na release ng application.
CH-J / Server Manager
1704 file
0.1.2-beta
Idinagdag: · Binuo:
1
//! Mandatory OpenPGP verification of Core update artifacts (port of `openPgpVerifier.js`).2
//!3
//! * the bundled public key must have the pinned primary fingerprint (trust anchor);4
//! * the primary key must be valid (bindings verify, not revoked, not expired);5
//! * the detached signature must be made by a signing-capable subkey bound to that primary6
//! key (valid, unrevoked, unexpired binding) — never by the primary key itself;7
//! * the artifact is hashed while streaming (artifacts up to 2 GiB).9
use crate::core::error::{CoreError, CoreResult};10
use pgp::composed::{Deserializable, DetachedSignature, SignedPublicKey};11
use pgp::packet::{Signature, SignatureType};12
use pgp::types::KeyDetails;13
use std::path::Path;14
use std::io::Read;15
use tokio_util::sync::CancellationToken;16
use std::time::{Duration, SystemTime};18
pub const TRUSTED_PRIMARY_FINGERPRINT: &str = "0D92778AD8ECF85C80E3924848F2433AD9CDF453";19
const MAX_PUBLIC_KEY_BYTES: u64 = 128 * 1024;20
const MAX_SIGNATURE_BYTES: u64 = 128 * 1024;22
fn error(code: &str, message: &str) -> CoreError {23
CoreError::new(code, message)24
}26
fn regular_file(path: &Path, label: &str, max: Option<u64>) -> CoreResult<()> {27
let meta = std::fs::symlink_metadata(path).map_err(|_| error("UPDATE_SIGNATURE_FILE_MISSING", &format!("{label} is missing.")))?;28
if meta.file_type().is_symlink() || !meta.is_file() {29
return Err(error("UPDATE_SIGNATURE_UNSAFE_FILE", &format!("{label} must be a regular file.")));30
}31
if meta.len() == 0 {32
return Err(error("UPDATE_SIGNATURE_EMPTY", &format!("{label} is empty.")));33
}34
if max.map(|m| meta.len() > m).unwrap_or(false) {35
return Err(error("UPDATE_SIGNATURE_TOO_LARGE", &format!("{label} is too large.")));36
}37
Ok(())38
}40
pub fn normalize_fingerprint(value: &str) -> String {41
value.chars().filter(|c| c.is_ascii_hexdigit()).collect::<String>().to_uppercase()42
}44
pub(crate) fn fingerprint_hex(fp: &pgp::types::Fingerprint) -> String {45
normalize_fingerprint(&format!("{fp:X}"))46
}48
pub(crate) fn system_time(ts: pgp::types::Timestamp) -> SystemTime {49
ts.into()50
}52
/// Signature created and not expired at `now`.53
pub(crate) fn signature_current(sig: &Signature, now: SystemTime) -> bool {54
let Some(created) = sig.created().map(system_time) else { return false };55
if created > now + Duration::from_secs(300) {56
return false;57
}58
match sig.signature_expiration_time() {59
Some(exp) if exp.as_secs() > 0 => created + std::time::Duration::from(exp) > now,60
_ => true,61
}62
}64
/// Key not expired at `now` according to the given (binding/self) signature.65
pub(crate) fn key_unexpired(key_created: SystemTime, sig: &Signature, now: SystemTime) -> bool {66
match sig.key_expiration_time() {67
Some(exp) if exp.as_secs() > 0 => key_created + std::time::Duration::from(exp) > now,68
_ => true,69
}70
}72
#[derive(Debug, Clone, serde::Serialize)]73
#[serde(rename_all = "camelCase")]74
pub struct Verification {75
pub valid: bool,76
pub primary_fingerprint: String,77
pub signing_fingerprints: Vec<String>,78
pub signature_created_at: Option<String>,79
}81
pub struct OpenPgpVerifier {82
pub public_key_path: std::path::PathBuf,83
pub trusted_primary: String,84
}86
/// Check cancellation before every artifact read, including buffered refills.87
struct CancellableReader<R> {88
reader: R,89
cancel: CancellationToken,90
}92
impl<R: Read> Read for CancellableReader<R> {93
fn read(&mut self, buffer: &mut [u8]) -> std::io::Result<usize> {94
if self.cancel.is_cancelled() {95
return Err(std::io::Error::new(std::io::ErrorKind::TimedOut, "Verification cancelled"));96
}97
self.reader.read(buffer)98
}99
}101
impl OpenPgpVerifier {102
pub fn new(public_key_path: std::path::PathBuf) -> Self {103
Self { public_key_path, trusted_primary: TRUSTED_PRIMARY_FINGERPRINT.to_string() }104
}106
fn load_key(&self, now: SystemTime) -> CoreResult<SignedPublicKey> {107
regular_file(&self.public_key_path, "Bundled OpenPGP public key", Some(MAX_PUBLIC_KEY_BYTES))?;108
let file = std::fs::File::open(&self.public_key_path).map_err(|_| error("UPDATE_PUBLIC_KEY_INVALID", "Bundled OpenPGP public key is malformed."))?;109
let (key, _) = SignedPublicKey::from_armor_single(std::io::BufReader::new(file)).map_err(|_| error("UPDATE_PUBLIC_KEY_INVALID", "Bundled OpenPGP public key is malformed."))?;110
let primary = fingerprint_hex(&key.primary_key.fingerprint());111
if primary != self.trusted_primary {112
return Err(error("UPDATE_PUBLIC_KEY_FINGERPRINT_MISMATCH", "Bundled OpenPGP primary fingerprint does not match the trust anchor."));113
}114
let invalid = || error("UPDATE_PUBLIC_KEY_INVALID", "Bundled OpenPGP primary key is revoked, expired, or invalid.");115
key.verify_bindings().map_err(|_| invalid())?;116
// Any valid key revocation signature revokes the primary key.117
for revocation in &key.details.revocation_signatures {118
if revocation.verify_key(&key.primary_key).is_ok() {119
return Err(invalid());120
}121
}122
// Bindings were verified above; expiry comes from the newest current self-certification.123
let created = system_time(key.primary_key.created_at());124
let newest = key125
.details126
.users127
.iter()128
.flat_map(|user| user.signatures.iter())129
.filter(|sig| signature_current(sig, now))130
.max_by_key(|sig| sig.created().map(system_time));131
match newest {132
Some(sig) if key_unexpired(created, sig, now) => Ok(key),133
_ => Err(invalid()),134
}135
}137
pub fn verify_file(&self, artifact: &Path, signature: &Path) -> CoreResult<Verification> {138
self.verify_file_cancellable(artifact, signature, CancellationToken::new())139
}141
pub fn verify_file_cancellable(&self, artifact: &Path, signature: &Path, cancel: CancellationToken) -> CoreResult<Verification> {142
regular_file(artifact, "Update artifact", None)?;143
regular_file(signature, "Detached OpenPGP signature", Some(MAX_SIGNATURE_BYTES))?;144
let now = SystemTime::now();145
let key = self.load_key(now)?;146
let primary_fp = fingerprint_hex(&key.primary_key.fingerprint());147
let sig_file = std::fs::File::open(signature).map_err(|_| error("UPDATE_SIGNATURE_MALFORMED", "Detached OpenPGP signature is malformed."))?;148
let (detached, _) = DetachedSignature::from_armor_single(std::io::BufReader::new(sig_file)).map_err(|_| error("UPDATE_SIGNATURE_MALFORMED", "Detached OpenPGP signature is malformed."))?;149
let sig = &detached.signature;150
let issuer_ids: Vec<String> = sig.issuer_key_id().iter().map(|id| hex::encode_upper(id.as_ref())).collect();151
let issuer_fps: Vec<String> = sig.issuer_fingerprint().iter().map(|fp| fingerprint_hex(fp)).collect();152
let untrusted = || error("UPDATE_SIGNATURE_UNTRUSTED_SIGNER", "The signature was not made by a valid signing subkey of the trusted primary key.");153
let subkey = key154
.public_subkeys155
.iter()156
.find(|sub| {157
let fp = fingerprint_hex(&sub.key.fingerprint());158
let id = hex::encode_upper(sub.key.legacy_key_id().as_ref());159
issuer_fps.contains(&fp) || issuer_ids.iter().any(|i| i.eq_ignore_ascii_case(&id))160
})161
.ok_or_else(|| {162
let primary_id = hex::encode_upper(key.primary_key.legacy_key_id().as_ref());163
if issuer_fps.contains(&primary_fp) || issuer_ids.iter().any(|i| i.eq_ignore_ascii_case(&primary_id)) {164
error("UPDATE_SIGNATURE_NOT_SIGNING_SUBKEY", "Updates must be signed by a valid signing subkey of the trusted primary key.")165
} else {166
untrusted()167
}168
})?;169
let subkey_invalid = || error("UPDATE_SIGNING_SUBKEY_INVALID", "The signing subkey is revoked, expired, or has an invalid binding.");170
let mut binding: Option<&Signature> = None;171
for candidate in &subkey.signatures {172
if candidate.verify_subkey_binding(&key.primary_key, &subkey.key).is_err() {173
continue;174
}175
match candidate.typ() {176
Some(SignatureType::SubkeyRevocation) => return Err(subkey_invalid()),177
Some(SignatureType::SubkeyBinding) if signature_current(candidate, now) => {178
if binding.map(|b| b.created().map(system_time) < candidate.created().map(system_time)).unwrap_or(true) {179
binding = Some(candidate);180
}181
}182
_ => {}183
}184
}185
let binding = binding.ok_or_else(subkey_invalid)?;186
if !key_unexpired(system_time(subkey.key.created_at()), binding, now) {187
return Err(subkey_invalid());188
}189
if !binding.key_flags().sign() {190
return Err(error("UPDATE_SIGNING_SUBKEY_CAPABILITY_INVALID", "The OpenPGP subkey does not have signing capability."));191
}192
if !signature_current(sig, now) {193
return Err(error("UPDATE_SIGNATURE_INVALID", "Detached OpenPGP signature verification failed."));194
}195
let data = std::fs::File::open(artifact).map_err(|_| error("UPDATE_SIGNATURE_FILE_MISSING", "Update artifact is missing."))?;196
sig.verify(&subkey.key, CancellableReader { reader: std::io::BufReader::with_capacity(1 << 20, data), cancel })197
.map_err(|_| error("UPDATE_SIGNATURE_INVALID", "Detached OpenPGP signature is not cryptographically valid."))?;198
let created = sig.created().map(|t| chrono::DateTime::<chrono::Utc>::from(system_time(t)).to_rfc3339_opts(chrono::SecondsFormat::Millis, true));199
Ok(Verification { valid: true, primary_fingerprint: primary_fp, signing_fingerprints: vec![fingerprint_hex(&subkey.key.fingerprint())], signature_created_at: created })200
}201
}203
#[cfg(test)]204
mod tests {205
use super::*;206
use std::path::PathBuf;208
fn verifier() -> OpenPgpVerifier {209
OpenPgpVerifier::new(Path::new(env!("CARGO_MANIFEST_DIR")).join("resources/signing/ch-j-signing-public.asc"))210
}212
fn fixture(name: &str) -> PathBuf {213
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/openpgp").join(name)214
}216
#[test]217
fn cancelled_reader_stops_before_more_io() {218
let cancel = CancellationToken::new();219
let mut reader = CancellableReader { reader: std::io::Cursor::new(vec![1; 32]), cancel: cancel.clone() };220
assert_eq!(reader.read(&mut [0; 4]).unwrap(), 4);221
cancel.cancel();222
assert_eq!(reader.read(&mut [0; 4]).unwrap_err().kind(), std::io::ErrorKind::TimedOut);223
assert_eq!(reader.reader.position(), 4);224
}226
#[test]227
fn verifies_real_release_signature() {228
let result = verifier().verify_file(&fixture("SHA512SUMS"), &fixture("SHA512SUMS.asc")).unwrap();229
assert!(result.valid);230
assert_eq!(result.primary_fingerprint, TRUSTED_PRIMARY_FINGERPRINT);231
assert_eq!(result.signing_fingerprints, vec!["F3EA1875CBCF63C89B2ED89744028841DE4EC7E4".to_string()]);232
}234
#[test]235
fn rejects_tampered_artifact_and_wrong_anchor() {236
let dir = tempfile::tempdir().unwrap();237
let tampered = dir.path().join("SHA512SUMS");238
let mut content = std::fs::read(fixture("SHA512SUMS")).unwrap();239
content[0] ^= 1;240
std::fs::write(&tampered, content).unwrap();241
assert_eq!(verifier().verify_file(&tampered, &fixture("SHA512SUMS.asc")).unwrap_err().code, "UPDATE_SIGNATURE_INVALID");242
let mut wrong = verifier();243
wrong.trusted_primary = "0".repeat(40);244
assert_eq!(wrong.verify_file(&fixture("SHA512SUMS"), &fixture("SHA512SUMS.asc")).unwrap_err().code, "UPDATE_PUBLIC_KEY_FINGERPRINT_MISMATCH");245
let garbage = dir.path().join("sig.asc");246
std::fs::write(&garbage, "-----BEGIN PGP SIGNATURE-----\n\nAAAA\n-----END PGP SIGNATURE-----\n").unwrap();247
assert_eq!(verifier().verify_file(&fixture("SHA512SUMS"), &garbage).unwrap_err().code, "UPDATE_SIGNATURE_MALFORMED");248
}249
}SHA-256
f135b4cb8e36f86b915f3ef039766a1e9223116328d9033c958ff25867b9f1ffCH-J Proprietary Software License 1.14 · Ibinibigay ang source sa ilalim ng CH-J Proprietary Software License 1.14. Hindi binabago ng pagiging available nito ang mga tuntunin ng lisensya at hindi ito nagbibigay ng karagdagang karapatan.
SHA-256 ng archive
ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f