CH-J Toolsserverhåndtering over SSH
Menu
Offentliggjort kilde

CH-J Tools

Gennemse mapper og filer til en specifik applikationsudgivelse.

Download kilde ZIP
CH-J / Server Manager 1704 filer
0.1.2-beta Tilføjet: · Bygget:
openpgp.rs Tilføjet: Dato ikke registreret 11,9 KB · 249 linjer
Kode Download fil
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 primary
6//! key (valid, unrevoked, unexpired binding) — never by the primary key itself;
7//! * the artifact is hashed while streaming (artifacts up to 2 GiB).
9use crate::core::error::{CoreError, CoreResult};
10use pgp::composed::{Deserializable, DetachedSignature, SignedPublicKey};
11use pgp::packet::{Signature, SignatureType};
12use pgp::types::KeyDetails;
13use std::path::Path;
14use std::io::Read;
15use tokio_util::sync::CancellationToken;
16use std::time::{Duration, SystemTime};
18pub const TRUSTED_PRIMARY_FINGERPRINT: &str = "0D92778AD8ECF85C80E3924848F2433AD9CDF453";
19const MAX_PUBLIC_KEY_BYTES: u64 = 128 * 1024;
20const MAX_SIGNATURE_BYTES: u64 = 128 * 1024;
22fn error(code: &str, message: &str) -> CoreError {
23 CoreError::new(code, message)
26fn 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(())
40pub fn normalize_fingerprint(value: &str) -> String {
41 value.chars().filter(|c| c.is_ascii_hexdigit()).collect::<String>().to_uppercase()
44pub(crate) fn fingerprint_hex(fp: &pgp::types::Fingerprint) -> String {
45 normalize_fingerprint(&format!("{fp:X}"))
48pub(crate) fn system_time(ts: pgp::types::Timestamp) -> SystemTime {
49 ts.into()
52/// Signature created and not expired at `now`.
53pub(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 }
64/// Key not expired at `now` according to the given (binding/self) signature.
65pub(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 }
72#[derive(Debug, Clone, serde::Serialize)]
73#[serde(rename_all = "camelCase")]
74pub struct Verification {
75 pub valid: bool,
76 pub primary_fingerprint: String,
77 pub signing_fingerprints: Vec<String>,
78 pub signature_created_at: Option<String>,
81pub struct OpenPgpVerifier {
82 pub public_key_path: std::path::PathBuf,
83 pub trusted_primary: String,
86/// Check cancellation before every artifact read, including buffered refills.
87struct CancellableReader<R> {
88 reader: R,
89 cancel: CancellationToken,
92impl<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 }
101impl 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 = key
125 .details
126 .users
127 .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 = key
154 .public_subkeys
155 .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 }
203#[cfg(test)]
204mod 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 }
SHA-256f135b4cb8e36f86b915f3ef039766a1e9223116328d9033c958ff25867b9f1ff

CH-J Proprietary Software License 1.14 · Kilden leveres under CH-J Proprietary Software License 1.14. Dens tilgængelighed ændrer ikke licensbetingelserne eller giver yderligere rettigheder.

Arkiv SHA-256ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f
Filer