1use std::collections::HashMap;
21use std::path::{Path, PathBuf};
22use std::sync::Arc;
23
24use cellos_core::{
25 cloud_event_v1_keyset_verification_failed, cloud_event_v1_keyset_verified,
26 load_trust_verify_keys_file, ports::EventSink, verify_signed_trust_keyset_chain,
27 verify_signed_trust_keyset_envelope, SignedTrustKeysetEnvelope,
28};
29use ed25519_dalek::VerifyingKey;
30use serde_json::Value;
31
32#[derive(Debug, Clone)]
38pub enum KeysetLoadOutcome {
39 NotConfigured,
41 Verified(KeysetVerifiedDetails),
43 Failed(KeysetVerificationFailedDetails),
47}
48
49#[derive(Debug, Clone)]
50pub struct KeysetVerifiedDetails {
51 pub keyset_id: String,
52 pub payload_digest: String,
53 pub verified_signer_kid: String,
54}
55
56#[derive(Debug, Clone)]
57pub struct KeysetVerificationFailedDetails {
58 pub attempted_keyset_basename: String,
59 pub reason: String,
60}
61
62pub fn load_trust_verify_keys_from_env(
76 require_trust_verify_keys: bool,
77) -> Result<Arc<HashMap<String, VerifyingKey>>, anyhow::Error> {
78 match std::env::var_os("CELLOS_TRUST_VERIFY_KEYS_PATH") {
79 None => {
80 if require_trust_verify_keys {
81 return Err(anyhow::anyhow!(
82 "CELLOS_REQUIRE_TRUST_VERIFY_KEYS is set but CELLOS_TRUST_VERIFY_KEYS_PATH is unset"
83 ));
84 }
85 tracing::debug!(
86 target: "cellos.supervisor.trust",
87 "CELLOS_TRUST_VERIFY_KEYS_PATH unset; supervisor runs with empty trust keyring"
88 );
89 Ok(Arc::new(HashMap::new()))
90 }
91 Some(path_os) => {
92 let path = PathBuf::from(&path_os);
93 let keys = load_trust_verify_keys_file(&path).map_err(|e| {
94 anyhow::anyhow!(
95 "CELLOS_TRUST_VERIFY_KEYS_PATH: cannot load '{}': {e}",
96 path.display()
97 )
98 })?;
99 tracing::info!(
100 target: "cellos.supervisor.trust",
101 path = %path.display(),
102 kid_count = keys.len(),
103 "trust verifying keys loaded"
104 );
105 Ok(Arc::new(keys))
106 }
107 }
108}
109
110pub fn load_and_verify_trust_keyset_from_env(
138 keys: &HashMap<String, VerifyingKey>,
139 require_trust_verify_keys: bool,
140 now: std::time::SystemTime,
141) -> Result<KeysetLoadOutcome, anyhow::Error> {
142 if let Some(chain_path_os) = std::env::var_os("CELLOS_TRUST_KEYSET_CHAIN_PATH") {
146 return load_and_verify_chain_from_env(chain_path_os, keys, require_trust_verify_keys, now);
147 }
148
149 let Some(path_os) = std::env::var_os("CELLOS_TRUST_KEYSET_PATH") else {
150 return Ok(KeysetLoadOutcome::NotConfigured);
151 };
152 let path = PathBuf::from(&path_os);
153 let basename = path
154 .file_name()
155 .map(|s| s.to_string_lossy().into_owned())
156 .unwrap_or_else(|| "(unknown)".to_string());
157
158 match attempt_load_and_verify(&path, keys, now) {
159 Ok(details) => {
160 tracing::info!(
161 target: "cellos.supervisor.trust",
162 keyset_id = %details.keyset_id,
163 payload_digest = %details.payload_digest,
164 verified_signer_kid = %details.verified_signer_kid,
165 "signed trust keyset envelope verified at supervisor startup"
166 );
167 Ok(KeysetLoadOutcome::Verified(details))
168 }
169 Err(reason_string) => {
170 if require_trust_verify_keys {
171 return Err(anyhow::anyhow!(
172 "CELLOS_TRUST_KEYSET_PATH: verification failed under CELLOS_REQUIRE_TRUST_VERIFY_KEYS=1: {reason_string}"
173 ));
174 }
175 tracing::warn!(
176 target: "cellos.supervisor.trust",
177 attempted_keyset_basename = %basename,
178 reason = %reason_string,
179 "signed trust keyset envelope verification failed; continuing in degraded mode"
180 );
181 Ok(KeysetLoadOutcome::Failed(KeysetVerificationFailedDetails {
182 attempted_keyset_basename: basename,
183 reason: reason_string,
184 }))
185 }
186 }
187}
188
189fn load_and_verify_chain_from_env(
202 chain_path_os: std::ffi::OsString,
203 keys: &HashMap<String, VerifyingKey>,
204 require_trust_verify_keys: bool,
205 now: std::time::SystemTime,
206) -> Result<KeysetLoadOutcome, anyhow::Error> {
207 let raw_str = chain_path_os.to_string_lossy().into_owned();
208 let paths: Vec<PathBuf> = raw_str
209 .split([',', '\n'])
210 .map(str::trim)
211 .filter(|s| !s.is_empty())
212 .map(PathBuf::from)
213 .collect();
214
215 if paths.is_empty() {
216 let reason = "CELLOS_TRUST_KEYSET_CHAIN_PATH set but parsed no envelope paths".to_string();
217 if require_trust_verify_keys {
218 return Err(anyhow::anyhow!(
219 "CELLOS_TRUST_KEYSET_CHAIN_PATH: verification failed under CELLOS_REQUIRE_TRUST_VERIFY_KEYS=1: {reason}"
220 ));
221 }
222 tracing::warn!(
223 target: "cellos.supervisor.trust",
224 reason = %reason,
225 "signed trust keyset chain verification failed; continuing in degraded mode"
226 );
227 return Ok(KeysetLoadOutcome::Failed(KeysetVerificationFailedDetails {
228 attempted_keyset_basename: "(empty chain)".into(),
229 reason,
230 }));
231 }
232
233 let head_basename = paths
236 .last()
237 .and_then(|p| p.file_name())
238 .map(|s| s.to_string_lossy().into_owned())
239 .unwrap_or_else(|| "(unknown)".to_string());
240
241 match attempt_load_and_verify_chain(&paths, keys, now) {
242 Ok(details) => {
243 tracing::info!(
244 target: "cellos.supervisor.trust",
245 envelope_count = paths.len(),
246 keyset_id = %details.keyset_id,
247 payload_digest = %details.payload_digest,
248 verified_signer_kid = %details.verified_signer_kid,
249 "verified {}-envelope chain (head digest: {})",
250 paths.len(),
251 details.payload_digest
252 );
253 Ok(KeysetLoadOutcome::Verified(details))
254 }
255 Err(reason_string) => {
256 if require_trust_verify_keys {
257 return Err(anyhow::anyhow!(
258 "CELLOS_TRUST_KEYSET_CHAIN_PATH: verification failed under CELLOS_REQUIRE_TRUST_VERIFY_KEYS=1: {reason_string}"
259 ));
260 }
261 tracing::warn!(
262 target: "cellos.supervisor.trust",
263 attempted_keyset_basename = %head_basename,
264 envelope_count = paths.len(),
265 reason = %reason_string,
266 "signed trust keyset chain verification failed; continuing in degraded mode"
267 );
268 Ok(KeysetLoadOutcome::Failed(KeysetVerificationFailedDetails {
269 attempted_keyset_basename: head_basename,
270 reason: reason_string,
271 }))
272 }
273 }
274}
275
276fn attempt_load_and_verify_chain(
279 paths: &[PathBuf],
280 keys: &HashMap<String, VerifyingKey>,
281 now: std::time::SystemTime,
282) -> Result<KeysetVerifiedDetails, String> {
283 let mut chain: Vec<SignedTrustKeysetEnvelope> = Vec::with_capacity(paths.len());
284 for path in paths {
285 let raw =
286 read_envelope_file(path).map_err(|e| format!("cannot read {}: {e}", path.display()))?;
287 let envelope: SignedTrustKeysetEnvelope = serde_json::from_str(&raw)
288 .map_err(|e| format!("JSON parse error in {}: {e}", path.display()))?;
289 chain.push(envelope);
290 }
291
292 let head_payload_bytes =
293 verify_signed_trust_keyset_chain(&chain, keys, now).map_err(|e| format!("{e}"))?;
294
295 let head_envelope = chain.last().expect("chain non-empty");
299 let verified_signer_kid =
300 pick_verified_signer_kid(head_envelope, &head_payload_bytes, keys, now)
301 .unwrap_or_else(|| "(unknown)".to_string());
302 let keyset_id =
303 decode_inner_keyset_id(&head_payload_bytes).unwrap_or_else(|| "(unknown)".into());
304
305 Ok(KeysetVerifiedDetails {
306 keyset_id,
307 payload_digest: head_envelope.payload_digest.clone(),
308 verified_signer_kid,
309 })
310}
311
312fn attempt_load_and_verify(
315 path: &Path,
316 keys: &HashMap<String, VerifyingKey>,
317 now: std::time::SystemTime,
318) -> Result<KeysetVerifiedDetails, String> {
319 let raw =
320 read_envelope_file(path).map_err(|e| format!("cannot read {}: {e}", path.display()))?;
321
322 let envelope: SignedTrustKeysetEnvelope = serde_json::from_str(&raw)
323 .map_err(|e| format!("JSON parse error in {}: {e}", path.display()))?;
324
325 let payload_bytes =
326 verify_signed_trust_keyset_envelope(&envelope, keys, now).map_err(|e| format!("{e}"))?;
327
328 let verified_signer_kid = pick_verified_signer_kid(&envelope, &payload_bytes, keys, now)
333 .unwrap_or_else(|| "(unknown)".to_string());
334
335 let keyset_id = decode_inner_keyset_id(&payload_bytes).unwrap_or_else(|| "(unknown)".into());
336
337 Ok(KeysetVerifiedDetails {
338 keyset_id,
339 payload_digest: envelope.payload_digest.clone(),
340 verified_signer_kid,
341 })
342}
343
344fn pick_verified_signer_kid(
352 envelope: &SignedTrustKeysetEnvelope,
353 payload_bytes: &[u8],
354 keys: &HashMap<String, VerifyingKey>,
355 now: std::time::SystemTime,
356) -> Option<String> {
357 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
358 use base64::Engine as _;
359 use chrono::DateTime;
360 use ed25519_dalek::Signature;
361
362 for sig in &envelope.signatures {
363 if sig.algorithm != "ed25519" {
364 continue;
365 }
366 let Some(verifying_key) = keys.get(&sig.signer_kid) else {
367 continue;
368 };
369 if !window_contains(now, sig.not_before.as_deref(), sig.not_after.as_deref()) {
370 continue;
371 }
372 let sig_b64 = sig.signature.trim_end_matches('=');
373 let Ok(sig_bytes) = URL_SAFE_NO_PAD.decode(sig_b64) else {
374 continue;
375 };
376 let Ok(sig_array) = <[u8; 64]>::try_from(sig_bytes.as_slice()) else {
377 continue;
378 };
379 let signature = Signature::from_bytes(&sig_array);
380 if verifying_key
381 .verify_strict(payload_bytes, &signature)
382 .is_ok()
383 {
384 return Some(sig.signer_kid.clone());
385 }
386 }
387 let _ = DateTime::<chrono::Utc>::from_timestamp(0, 0);
389 None
390}
391
392fn window_contains(
394 now: std::time::SystemTime,
395 not_before: Option<&str>,
396 not_after: Option<&str>,
397) -> bool {
398 use chrono::DateTime;
399 let now_chrono: DateTime<chrono::Utc> = now.into();
400 if let Some(nb) = not_before {
401 match DateTime::parse_from_rfc3339(nb) {
402 Ok(t) => {
403 if now_chrono < t.with_timezone(&chrono::Utc) {
404 return false;
405 }
406 }
407 Err(_) => return false,
408 }
409 }
410 if let Some(na) = not_after {
411 match DateTime::parse_from_rfc3339(na) {
412 Ok(t) => {
413 if now_chrono > t.with_timezone(&chrono::Utc) {
414 return false;
415 }
416 }
417 Err(_) => return false,
418 }
419 }
420 true
421}
422
423fn decode_inner_keyset_id(payload_bytes: &[u8]) -> Option<String> {
427 let v: Value = serde_json::from_slice(payload_bytes).ok()?;
428 v.as_object()?.get("keysetId")?.as_str().map(String::from)
429}
430
431fn read_envelope_file(path: &Path) -> Result<String, std::io::Error> {
434 #[cfg(unix)]
435 {
436 use std::io::Read;
437 use std::os::unix::fs::OpenOptionsExt;
438 let mut opts = std::fs::OpenOptions::new();
439 opts.read(true);
440 opts.custom_flags(libc::O_RDONLY | libc::O_NOFOLLOW);
441 let mut file = opts.open(path)?;
442 let mut buf = String::new();
443 file.read_to_string(&mut buf)?;
444 Ok(buf)
445 }
446 #[cfg(not(unix))]
447 {
448 std::fs::read_to_string(path)
449 }
450}
451
452pub async fn emit_keyset_outcome(
462 outcome: &KeysetLoadOutcome,
463 event_sink: &Arc<dyn EventSink>,
464 jsonl_sink: Option<&Arc<dyn EventSink>>,
465 now: chrono::DateTime<chrono::Utc>,
466) -> Result<(), anyhow::Error> {
467 let timestamp = now.to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
468 match outcome {
469 KeysetLoadOutcome::NotConfigured => Ok(()),
470 KeysetLoadOutcome::Verified(details) => {
471 let envelope = cloud_event_v1_keyset_verified(
472 "cellos-supervisor",
473 ×tamp,
474 &details.keyset_id,
475 &details.payload_digest,
476 &details.verified_signer_kid,
477 ×tamp,
478 None,
479 )?;
480 event_sink
481 .emit(&envelope)
482 .await
483 .map_err(|e| anyhow::anyhow!("emit keyset_verified: {e}"))?;
484 if let Some(secondary) = jsonl_sink {
485 secondary
486 .emit(&envelope)
487 .await
488 .map_err(|e| anyhow::anyhow!("emit keyset_verified to jsonl sink: {e}"))?;
489 }
490 Ok(())
491 }
492 KeysetLoadOutcome::Failed(details) => {
493 let envelope = cloud_event_v1_keyset_verification_failed(
494 "cellos-supervisor",
495 ×tamp,
496 &details.attempted_keyset_basename,
497 &details.reason,
498 ×tamp,
499 None,
500 )?;
501 event_sink
502 .emit(&envelope)
503 .await
504 .map_err(|e| anyhow::anyhow!("emit keyset_verification_failed: {e}"))?;
505 if let Some(secondary) = jsonl_sink {
506 secondary.emit(&envelope).await.map_err(|e| {
507 anyhow::anyhow!("emit keyset_verification_failed to jsonl sink: {e}")
508 })?;
509 }
510 Ok(())
511 }
512 }
513}
514
515#[cfg(test)]
516mod tests {
517 use super::{
521 load_and_verify_trust_keyset_from_env, load_trust_verify_keys_from_env, KeysetLoadOutcome,
522 };
523 use std::sync::{Mutex, MutexGuard};
524
525 static ENV_MUTEX: Mutex<()> = Mutex::new(());
526
527 fn lock_env() -> MutexGuard<'static, ()> {
528 ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner())
529 }
530
531 #[test]
532 fn unset_path_with_require_unset_yields_empty_map() {
533 let _guard = lock_env();
534 std::env::remove_var("CELLOS_TRUST_VERIFY_KEYS_PATH");
535 let keys = load_trust_verify_keys_from_env(false).expect("legacy posture");
536 assert!(keys.is_empty());
537 }
538
539 #[test]
540 fn unset_path_with_require_set_errors() {
541 let _guard = lock_env();
542 std::env::remove_var("CELLOS_TRUST_VERIFY_KEYS_PATH");
543 let err = load_trust_verify_keys_from_env(true).expect_err("require set + unset path");
544 assert!(format!("{err}").contains("CELLOS_TRUST_VERIFY_KEYS_PATH"));
545 }
546
547 #[test]
548 fn keyset_path_unset_returns_not_configured() {
549 let _guard = lock_env();
550 std::env::remove_var("CELLOS_TRUST_KEYSET_PATH");
551 let outcome = load_and_verify_trust_keyset_from_env(
552 &Default::default(),
553 false,
554 std::time::SystemTime::now(),
555 )
556 .expect("unset path + fail-open");
557 assert!(matches!(outcome, KeysetLoadOutcome::NotConfigured));
558 }
559}