1use crate::{ArtifactDescriptor, UpdateError, UpdateResult};
12use ed25519_dalek::{Signature, Verifier, VerifyingKey};
13use std::collections::{BTreeMap, BTreeSet};
14#[cfg(feature = "allow-unsigned-local-artifacts")]
15use std::path::{Path, PathBuf};
16
17pub trait ArtifactAuthenticityVerifier: Send + Sync {
19 fn verify(&self, artifact: &ArtifactDescriptor) -> UpdateResult<()>;
21}
22
23#[cfg(feature = "allow-unsigned-local-artifacts")]
29#[derive(Debug, Clone)]
30pub struct UnsignedLocalArtifactVerifier {
31 root: PathBuf,
32}
33
34#[cfg(feature = "allow-unsigned-local-artifacts")]
35impl UnsignedLocalArtifactVerifier {
36 pub fn new(root: impl AsRef<Path>) -> UpdateResult<Self> {
38 let root = canonical_private_root(root.as_ref())?;
39 Ok(Self { root })
40 }
41}
42
43#[cfg(feature = "allow-unsigned-local-artifacts")]
44impl ArtifactAuthenticityVerifier for UnsignedLocalArtifactVerifier {
45 fn verify(&self, artifact: &ArtifactDescriptor) -> UpdateResult<()> {
46 let raw_path = artifact
47 .artifact_reference()
48 .strip_prefix("file:")
49 .ok_or_else(|| {
50 UpdateError::Authenticity(
51 "unsigned local artifacts require a file: reference".to_string(),
52 )
53 })?;
54 let path = Path::new(raw_path);
55 reject_symlink_components(path)?;
56 let canonical = std::fs::canonicalize(path)
57 .map_err(|error| UpdateError::Authenticity(error.to_string()))?;
58 if !canonical.starts_with(&self.root) {
59 return Err(UpdateError::Authenticity(
60 "unsigned artifact escapes its canonical local root".to_string(),
61 ));
62 }
63 validate_private_artifact(&canonical)
64 }
65}
66
67#[derive(Debug, Clone, Default)]
69pub struct Ed25519ArtifactVerifier {
70 trust_roots: BTreeMap<String, TrustedSigningKey>,
71}
72
73#[derive(Debug, Clone)]
74struct TrustedSigningKey {
75 key: VerifyingKey,
76 status: SigningKeyStatus,
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum SigningKeyStatus {
82 Active,
84 Deprecated,
86 Revoked,
88}
89
90impl Ed25519ArtifactVerifier {
91 pub fn new() -> Self {
93 Self::default()
94 }
95
96 pub fn add_trust_root(
98 &mut self,
99 key_id: impl Into<String>,
100 public_key: [u8; 32],
101 ) -> UpdateResult<()> {
102 let key_id = key_id.into();
103 validate_key_id(&key_id)?;
104 let verifying_key = VerifyingKey::from_bytes(&public_key)
105 .map_err(|error| UpdateError::Authenticity(error.to_string()))?;
106 self.trust_roots.insert(
107 key_id,
108 TrustedSigningKey {
109 key: verifying_key,
110 status: SigningKeyStatus::Active,
111 },
112 );
113 Ok(())
114 }
115
116 pub fn add_trust_root_hex(
118 &mut self,
119 key_id: impl Into<String>,
120 public_key: &str,
121 ) -> UpdateResult<()> {
122 self.add_trust_root(key_id, decode_hex::<32>(public_key)?)
123 }
124
125 pub fn set_trust_root_status(
127 &mut self,
128 key_id: &str,
129 status: SigningKeyStatus,
130 ) -> UpdateResult<()> {
131 let key = self.trust_roots.get_mut(key_id).ok_or_else(|| {
132 UpdateError::Authenticity(format!("signing key `{key_id}` is not configured"))
133 })?;
134 key.status = status;
135 Ok(())
136 }
137
138 pub fn trust_root_count(&self) -> usize {
140 self.trust_roots.len()
141 }
142}
143
144impl ArtifactAuthenticityVerifier for Ed25519ArtifactVerifier {
145 fn verify(&self, artifact: &ArtifactDescriptor) -> UpdateResult<()> {
146 let key_id = artifact.signing_key_id().ok_or_else(|| {
147 UpdateError::Authenticity("signed artifact metadata is required".to_string())
148 })?;
149 let signature = artifact.ed25519_signature().ok_or_else(|| {
150 UpdateError::Authenticity("signed artifact metadata is required".to_string())
151 })?;
152 let trusted_key = self.trust_roots.get(key_id).ok_or_else(|| {
153 UpdateError::Authenticity(format!("signing key `{key_id}` is not trusted"))
154 })?;
155 if trusted_key.status == SigningKeyStatus::Revoked {
156 return Err(UpdateError::Authenticity(format!(
157 "signing key `{key_id}` is revoked"
158 )));
159 }
160 let signature_bytes = decode_hex::<64>(signature)?;
161 let signature = Signature::from_bytes(&signature_bytes);
162 trusted_key
163 .key
164 .verify(&artifact_signing_payload(artifact), &signature)
165 .map_err(|_| UpdateError::Authenticity("signature is invalid".to_string()))
166 }
167}
168
169#[derive(Debug, Clone, Default)]
171pub struct ArtifactTrustPolicy {
172 allowed_channels: BTreeSet<String>,
173 allowed_origins: BTreeSet<String>,
174}
175
176impl ArtifactTrustPolicy {
177 pub fn new() -> Self {
179 Self::default()
180 }
181
182 pub fn allow_channel(mut self, channel: impl Into<String>) -> UpdateResult<Self> {
184 let channel = channel.into();
185 validate_policy_value("channel", &channel)?;
186 self.allowed_channels.insert(channel);
187 Ok(self)
188 }
189
190 pub fn allow_origin(mut self, origin: impl Into<String>) -> UpdateResult<Self> {
192 let origin = normalize_origin(&origin.into())?;
193 self.allowed_origins.insert(origin);
194 Ok(self)
195 }
196
197 pub fn verify(&self, artifact: &ArtifactDescriptor) -> UpdateResult<()> {
199 if !self.allowed_channels.contains(artifact.channel()) {
200 return Err(UpdateError::Authenticity(format!(
201 "update channel `{}` is not allowed",
202 artifact.channel()
203 )));
204 }
205 let origin = artifact_origin(artifact.artifact_reference())?;
206 if !self.allowed_origins.contains(&origin) {
207 return Err(UpdateError::Authenticity(format!(
208 "artifact origin `{origin}` is not allowed"
209 )));
210 }
211 Ok(())
212 }
213}
214
215#[derive(Debug, Clone)]
217pub struct PolicyArtifactVerifier<V> {
218 policy: ArtifactTrustPolicy,
219 verifier: V,
220}
221
222impl<V> PolicyArtifactVerifier<V> {
223 pub fn new(policy: ArtifactTrustPolicy, verifier: V) -> Self {
225 Self { policy, verifier }
226 }
227}
228
229impl<V> ArtifactAuthenticityVerifier for PolicyArtifactVerifier<V>
230where
231 V: ArtifactAuthenticityVerifier,
232{
233 fn verify(&self, artifact: &ArtifactDescriptor) -> UpdateResult<()> {
234 self.policy.verify(artifact)?;
235 self.verifier.verify(artifact)
236 }
237}
238
239pub fn artifact_signing_payload(artifact: &ArtifactDescriptor) -> Vec<u8> {
241 [
242 ("application_id", artifact.application_id().as_str()),
243 ("application_version", artifact.application_version()),
244 ("build_id", artifact.build_id().as_str()),
245 ("channel", artifact.channel()),
246 ("runtime_requirement", artifact.runtime_requirement()),
247 ("protocol_version", artifact.protocol_version()),
248 ("artifact_reference", artifact.artifact_reference()),
249 ("sha256", artifact.sha256()),
250 ("size_bytes", &artifact.size_bytes().to_string()),
251 ]
252 .into_iter()
253 .flat_map(|(name, value)| {
254 let mut field = Vec::with_capacity(name.len() + value.len() + 2);
255 field.extend_from_slice(name.as_bytes());
256 field.push(b'=');
257 field.extend_from_slice(value.as_bytes());
258 field.push(b'\n');
259 field
260 })
261 .collect()
262}
263
264fn validate_key_id(key_id: &str) -> UpdateResult<()> {
265 if key_id.trim().is_empty() || key_id.len() > 128 || key_id.chars().any(char::is_control) {
266 return Err(UpdateError::Authenticity(
267 "trust root key identity is invalid".to_string(),
268 ));
269 }
270 Ok(())
271}
272
273fn validate_policy_value(name: &str, value: &str) -> UpdateResult<()> {
274 if value.trim().is_empty() || value.len() > 2_048 || value.chars().any(char::is_control) {
275 return Err(UpdateError::Authenticity(format!(
276 "artifact policy {name} is invalid"
277 )));
278 }
279 Ok(())
280}
281
282fn artifact_origin(reference: &str) -> UpdateResult<String> {
283 let (scheme, rest) = reference
284 .split_once(':')
285 .ok_or_else(|| UpdateError::Authenticity("artifact reference has no scheme".to_string()))?;
286 match scheme {
287 "file" => Ok("file:".to_string()),
288 "https" => {
289 let authority = rest.strip_prefix("//").ok_or_else(|| {
290 UpdateError::Authenticity("HTTPS artifact reference has no authority".to_string())
291 })?;
292 let authority = authority.split('/').next().unwrap_or_default();
293 if authority.is_empty() || authority.contains('@') {
294 return Err(UpdateError::Authenticity(
295 "HTTPS artifact authority is invalid".to_string(),
296 ));
297 }
298 Ok(format!("https://{}", authority.to_ascii_lowercase()))
299 }
300 _ => Err(UpdateError::Authenticity(format!(
301 "artifact scheme `{scheme}` is not production-supported"
302 ))),
303 }
304}
305
306fn normalize_origin(origin: &str) -> UpdateResult<String> {
307 validate_policy_value("origin", origin)?;
308 if origin == "file:" {
309 return Ok(origin.to_string());
310 }
311 let normalized = artifact_origin(origin)?;
312 if normalized != origin.trim_end_matches('/').to_ascii_lowercase() {
313 return Err(UpdateError::Authenticity(
314 "allowed origin must contain only scheme and authority".to_string(),
315 ));
316 }
317 Ok(normalized)
318}
319
320fn decode_hex<const N: usize>(value: &str) -> UpdateResult<[u8; N]> {
321 if value.len() != N * 2 {
322 return Err(UpdateError::Authenticity(
323 "signature has an invalid length".to_string(),
324 ));
325 }
326 let mut decoded = [0_u8; N];
327 for (index, chunk) in value.as_bytes().chunks_exact(2).enumerate() {
328 let text = std::str::from_utf8(chunk)
329 .map_err(|_| UpdateError::Authenticity("signature is not hexadecimal".to_string()))?;
330 decoded[index] = u8::from_str_radix(text, 16)
331 .map_err(|_| UpdateError::Authenticity("signature is not hexadecimal".to_string()))?;
332 }
333 Ok(decoded)
334}
335
336#[cfg(feature = "allow-unsigned-local-artifacts")]
337fn canonical_private_root(root: &Path) -> UpdateResult<PathBuf> {
338 reject_symlink_components(root)?;
339 let canonical = std::fs::canonicalize(root)
340 .map_err(|error| UpdateError::Authenticity(error.to_string()))?;
341 let metadata = std::fs::symlink_metadata(&canonical)
342 .map_err(|error| UpdateError::Authenticity(error.to_string()))?;
343 if !metadata.is_dir() {
344 return Err(UpdateError::Authenticity(
345 "unsigned artifact root is not a directory".to_string(),
346 ));
347 }
348 validate_owner(&metadata)?;
349 Ok(canonical)
350}
351
352#[cfg(feature = "allow-unsigned-local-artifacts")]
353fn reject_symlink_components(path: &Path) -> UpdateResult<()> {
354 let mut current = PathBuf::new();
355 for component in path.components() {
356 current.push(component);
357 match std::fs::symlink_metadata(¤t) {
358 Ok(metadata) if metadata.file_type().is_symlink() => {
359 return Err(UpdateError::Authenticity(
360 "unsigned artifact path contains a symlink".to_string(),
361 ));
362 }
363 Ok(_) => {}
364 Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
365 Err(error) => return Err(UpdateError::Authenticity(error.to_string())),
366 }
367 }
368 Ok(())
369}
370
371#[cfg(feature = "allow-unsigned-local-artifacts")]
372fn validate_private_artifact(path: &Path) -> UpdateResult<()> {
373 let metadata = std::fs::symlink_metadata(path)
374 .map_err(|error| UpdateError::Authenticity(error.to_string()))?;
375 if metadata.file_type().is_symlink() || !metadata.is_file() {
376 return Err(UpdateError::Authenticity(
377 "unsigned artifact is not a regular file".to_string(),
378 ));
379 }
380 validate_owner(&metadata)
381}
382
383#[cfg(all(feature = "allow-unsigned-local-artifacts", unix))]
384fn validate_owner(metadata: &std::fs::Metadata) -> UpdateResult<()> {
385 use std::os::unix::fs::{MetadataExt, PermissionsExt};
386 if metadata.uid() != unsafe { libc::geteuid() } || metadata.permissions().mode() & 0o077 != 0 {
387 return Err(UpdateError::Authenticity(
388 "unsigned artifact is not owner-controlled".to_string(),
389 ));
390 }
391 Ok(())
392}
393
394#[cfg(all(feature = "allow-unsigned-local-artifacts", not(unix)))]
395fn validate_owner(_metadata: &std::fs::Metadata) -> UpdateResult<()> {
396 Err(UpdateError::Authenticity(
397 "unsigned local artifacts are unsupported on this platform".to_string(),
398 ))
399}