1use std::{
10 collections::{BTreeMap, BTreeSet},
11 io::{Cursor, Read, Seek, Write},
12};
13
14use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
15use rill_runtime_protocol::ReleaseIndexPayload;
16use serde::{Deserialize, Serialize};
17use serde_json::Value;
18use sha2::{Digest, Sha256};
19use thiserror::Error;
20use zip::{ZipArchive, ZipWriter, write::SimpleFileOptions};
21
22const MANIFEST_PATH: &str = "manifest.json";
23const CHECKSUMS_PATH: &str = "checksums.json";
24const SIGNATURE_PATH: &str = "META-INF/signature.ed25519";
25
26#[derive(Debug, Default, Clone)]
27pub struct TrustStore(pub BTreeMap<String, VerifyingKey>);
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30#[serde(rename_all = "camelCase", deny_unknown_fields)]
31pub(crate) struct Checksums {
32 schema_version: u32,
33 files: BTreeMap<String, String>,
34}
35
36#[derive(Debug, Error)]
37pub enum ArchiveError {
38 #[error("zip error: {0}")]
39 Zip(#[from] zip::result::ZipError),
40 #[error("I/O error: {0}")]
41 Io(#[from] std::io::Error),
42 #[error("JSON error: {0}")]
43 Json(#[from] serde_json::Error),
44 #[error("unsafe package path {0}")]
45 UnsafePath(String),
46 #[error("forbidden package file {0}")]
47 Forbidden(String),
48 #[error("duplicate package file {0}")]
49 Duplicate(String),
50 #[error("package exceeded {0} limit")]
51 Limit(&'static str),
52 #[error("missing package file {0}")]
53 Missing(&'static str),
54 #[error("missing package file {0}")]
55 MissingOwned(String),
56 #[error("checksum coverage does not exactly match the payload")]
57 ChecksumCoverage,
58 #[error("checksum mismatch for {0}")]
59 Digest(String),
60 #[error("unknown publisher key")]
61 UnknownKey,
62 #[error("signature verification failed")]
63 Signature,
64}
65
66#[derive(Debug, Error)]
67pub enum ReleaseIndexError {
68 #[error("JSON error: {0}")]
69 Json(#[from] serde_json::Error),
70 #[error("invalid release index: {0}")]
71 Manifest(String),
72 #[error("unknown release-index publisher key")]
73 UnknownKey,
74 #[error("release-index signature verification failed")]
75 Signature,
76 #[error("canonical JSON error: {0}")]
77 Canonical(ArchiveError),
78}
79
80#[derive(Debug, Clone, Copy)]
82pub(crate) struct ArchiveLimits {
83 pub max_files: usize,
84 pub max_file_bytes: u64,
85 pub max_total_bytes: u64,
86 pub max_compression_ratio: u64,
87}
88
89pub(crate) struct PackPaths {
91 pub manifest: &'static str,
92 pub checksums: &'static str,
93 pub signature: &'static str,
94}
95
96pub(crate) const DEFAULT_PATHS: PackPaths = PackPaths {
97 manifest: MANIFEST_PATH,
98 checksums: CHECKSUMS_PATH,
99 signature: SIGNATURE_PATH,
100};
101
102pub fn canonical_json(bytes: &[u8]) -> Result<Vec<u8>, ArchiveError> {
103 fn sort(value: Value) -> Value {
104 match value {
105 Value::Object(map) => Value::Object(
106 map.into_iter()
107 .map(|(key, value)| (key, sort(value)))
108 .collect(),
109 ),
110 Value::Array(items) => Value::Array(items.into_iter().map(sort).collect()),
111 other => other,
112 }
113 }
114 let value: Value = serde_json::from_slice(bytes)?;
115 Ok(serde_json::to_vec(&sort(value))?)
116}
117
118pub fn sign_release_index(
119 payload: ReleaseIndexPayload,
120 signing_key: &SigningKey,
121) -> Result<rill_runtime_protocol::SignedReleaseIndex, ReleaseIndexError> {
122 validate_release_payload(&payload)?;
123 let serialized = serde_json::to_vec(&payload)?;
124 let canonical = canonical_json(&serialized).map_err(ReleaseIndexError::Canonical)?;
125 let signature = hex::encode(signing_key.sign(&canonical).to_bytes());
126 Ok(rill_runtime_protocol::SignedReleaseIndex { payload, signature })
127}
128
129pub fn verify_release_index(
130 index: &rill_runtime_protocol::SignedReleaseIndex,
131 trust: &TrustStore,
132) -> Result<(), ReleaseIndexError> {
133 validate_release_payload(&index.payload)?;
134 let signature_bytes =
135 hex::decode(&index.signature).map_err(|_| ReleaseIndexError::Signature)?;
136 let signature =
137 Signature::from_slice(&signature_bytes).map_err(|_| ReleaseIndexError::Signature)?;
138 let key = trust
139 .0
140 .get(&index.payload.publisher_key_id)
141 .ok_or(ReleaseIndexError::UnknownKey)?;
142 let serialized = serde_json::to_vec(&index.payload)?;
143 let canonical = canonical_json(&serialized).map_err(ReleaseIndexError::Canonical)?;
144 key.verify(&canonical, &signature)
145 .map_err(|_| ReleaseIndexError::Signature)
146}
147
148fn validate_release_payload(payload: &ReleaseIndexPayload) -> Result<(), ReleaseIndexError> {
149 payload
150 .validate_shape()
151 .map_err(|message| ReleaseIndexError::Manifest(message.into()))?;
152 let mut identities = BTreeSet::new();
153 for artifact in &payload.artifacts {
154 semver::Version::parse(&artifact.version).map_err(|error| {
155 ReleaseIndexError::Manifest(format!("invalid artifact version: {error}"))
156 })?;
157 let identity = (
158 artifact.kind.clone(),
159 artifact.id.clone(),
160 artifact.target_os.clone(),
161 artifact.target_arch.clone(),
162 artifact.handler_api_version,
163 );
164 if !identities.insert(identity) {
165 return Err(ReleaseIndexError::Manifest(
166 "duplicate release artifact identity".into(),
167 ));
168 }
169 }
170 Ok(())
171}
172
173pub(crate) fn read_archive<R: Read + Seek>(
176 reader: R,
177 allowed: &[&str],
178 limits: ArchiveLimits,
179) -> Result<BTreeMap<String, Vec<u8>>, ArchiveError> {
180 let mut archive = ZipArchive::new(reader)?;
181 if archive.len() > limits.max_files {
182 return Err(ArchiveError::Limit("file count"));
183 }
184 let mut total = 0u64;
185 let mut files = BTreeMap::new();
186 for index in 0..archive.len() {
187 let mut entry = archive.by_index(index)?;
188 if entry.is_dir() {
189 continue;
190 }
191 let name = entry.name().to_string();
192 validate_path(&name)?;
193 if !allowed.iter().any(|allowed| *allowed == name) {
194 return Err(ArchiveError::Forbidden(name));
195 }
196 if entry.size() > limits.max_file_bytes {
197 return Err(ArchiveError::Limit("file size"));
198 }
199 let compressed = entry.compressed_size();
200 if compressed > 0 && entry.size() / compressed > limits.max_compression_ratio {
201 return Err(ArchiveError::Limit("compression ratio"));
202 }
203 total = total
204 .checked_add(entry.size())
205 .ok_or(ArchiveError::Limit("total size"))?;
206 if total > limits.max_total_bytes {
207 return Err(ArchiveError::Limit("total size"));
208 }
209 let mut bytes = Vec::with_capacity(entry.size() as usize);
210 entry.read_to_end(&mut bytes)?;
211 if files.insert(name.clone(), bytes).is_some() {
212 return Err(ArchiveError::Duplicate(name));
213 }
214 }
215 Ok(files)
216}
217
218pub(crate) fn verify_checksums_and_signature(
223 files: &BTreeMap<String, Vec<u8>>,
224 paths: &PackPaths,
225 checksum_payload_names: &[&str],
226 publisher_key_id: &str,
227 trust: &TrustStore,
228) -> Result<(), ArchiveError> {
229 let checksum_bytes = files
230 .get(paths.checksums)
231 .ok_or(ArchiveError::Missing(paths.checksums))?;
232 let checksums: Checksums = serde_json::from_slice(checksum_bytes)?;
233 if checksums.schema_version != 1 {
234 return Err(ArchiveError::Missing("checksum schema version"));
235 }
236 let mut expected_names: Vec<String> = checksum_payload_names
237 .iter()
238 .map(|s| s.to_string())
239 .collect();
240 expected_names.sort();
241 let actual_names: Vec<String> = checksums.files.keys().cloned().collect();
242 if actual_names != expected_names {
243 return Err(ArchiveError::ChecksumCoverage);
244 }
245 for (name, expected) in &checksums.files {
246 let bytes = files
247 .get(name)
248 .ok_or_else(|| ArchiveError::MissingOwned(name.clone()))?;
249 let actual = hex::encode(Sha256::digest(bytes));
250 if &actual != expected {
251 return Err(ArchiveError::Digest(name.clone()));
252 }
253 }
254 let raw_signature = files
255 .get(paths.signature)
256 .ok_or(ArchiveError::Missing(paths.signature))?;
257 let signature = Signature::from_slice(raw_signature).map_err(|_| ArchiveError::Signature)?;
258 let key = trust
259 .0
260 .get(publisher_key_id)
261 .ok_or(ArchiveError::UnknownKey)?;
262 let manifest_bytes = files
263 .get(paths.manifest)
264 .ok_or(ArchiveError::Missing(paths.manifest))?;
265 let mut message = canonical_json(manifest_bytes)?;
266 message.push(b'\n');
267 message.extend(canonical_json(checksum_bytes)?);
268 key.verify(&message, &signature)
269 .map_err(|_| ArchiveError::Signature)
270}
271
272pub(crate) fn build_signed_archive(
275 manifest_bytes: &[u8],
276 payload_name: &str,
277 payload_bytes: &[u8],
278 signing_key: &SigningKey,
279) -> Result<Vec<u8>, ArchiveError> {
280 let checksums = Checksums {
281 schema_version: 1,
282 files: BTreeMap::from([
283 (
284 MANIFEST_PATH.into(),
285 hex::encode(Sha256::digest(manifest_bytes)),
286 ),
287 (
288 payload_name.into(),
289 hex::encode(Sha256::digest(payload_bytes)),
290 ),
291 ]),
292 };
293 let checksum_bytes = serde_json::to_vec_pretty(&checksums)?;
294 let mut message = canonical_json(manifest_bytes)?;
295 message.push(b'\n');
296 message.extend(canonical_json(&checksum_bytes)?);
297 let signature = signing_key.sign(&message).to_bytes();
298
299 let mut output = Cursor::new(Vec::new());
300 {
301 let mut archive = ZipWriter::new(&mut output);
302 let options = SimpleFileOptions::default()
303 .compression_method(zip::CompressionMethod::Deflated)
304 .unix_permissions(0o644);
305 for (name, bytes) in [
306 (MANIFEST_PATH, manifest_bytes),
307 (payload_name, payload_bytes),
308 (CHECKSUMS_PATH, checksum_bytes.as_slice()),
309 (SIGNATURE_PATH, signature.as_slice()),
310 ] {
311 archive.start_file(name, options)?;
312 archive.write_all(bytes)?;
313 }
314 archive.finish()?;
315 }
316 Ok(output.into_inner())
317}
318
319fn validate_path(name: &str) -> Result<(), ArchiveError> {
320 if name.starts_with('/')
321 || name.contains('\\')
322 || name
323 .split('/')
324 .any(|part| part.is_empty() || part == "." || part == "..")
325 {
326 return Err(ArchiveError::UnsafePath(name.into()));
327 }
328 Ok(())
329}