From 8cec5a648658ba1c41243fcaaa99a8be7679e88bb115b2a921ed5c9c1f0da27a Mon Sep 17 00:00:00 2001 From: Tyler J King Date: Wed, 15 Apr 2026 19:57:48 -0400 Subject: [PATCH] feat(org-ops): manifest signature check at load time Add ManifestMeta to manifest loader for signature validation: - load_manifest_with_meta() rejects unsigned manifests when signatures_required=true and signature_valid=false - Clear error message directs operator to quorum administrator - Backward compatible: load_manifest() passes default meta (no check) 2 new tests for signature rejection and acceptance. Signed-off-by: Tyler King Signed-off-by: Tyler J King --- org-ops-core/src/manifest_loader.rs | 54 +++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/org-ops-core/src/manifest_loader.rs b/org-ops-core/src/manifest_loader.rs index b2e61e9..f8b0d70 100644 --- a/org-ops-core/src/manifest_loader.rs +++ b/org-ops-core/src/manifest_loader.rs @@ -52,10 +52,41 @@ pub struct ManifestLoadResult { /// Reads the manifest JSON (from ConfigMap mount or API), filters /// entries by the session's ShellClass and delegation scope, and /// optionally verifies on-disk binary hashes. +/// Optional signature status from the reconciler. +/// Passed alongside manifest_json when the ConfigMap includes it. +#[derive(Debug, Clone, Default)] +pub struct ManifestMeta { + /// Whether the reconciler verified all required witness signatures. + pub signature_valid: bool, + /// Whether witness signatures are required for this manifest. + pub signatures_required: bool, +} + pub fn load_manifest( ctx: &SessionContext, manifest_json: &str, ) -> Result { + load_manifest_with_meta(ctx, manifest_json, &ManifestMeta::default()) +} + +/// Load manifest with optional signature metadata. +/// +/// If `meta.signatures_required` is true and `meta.signature_valid` is +/// false, the load is rejected (unsigned manifests don't take effect). +pub fn load_manifest_with_meta( + ctx: &SessionContext, + manifest_json: &str, + meta: &ManifestMeta, +) -> Result { + // Check signature if required + if meta.signatures_required && !meta.signature_valid { + return Err( + "manifest not signed by all required Accord witnesses — \ + contact your quorum administrator" + .into(), + ); + } + let entries: Vec = serde_json::from_str(manifest_json).map_err(|e| format!("manifest parse error: {e}"))?; @@ -248,6 +279,29 @@ mod tests { assert!(result.is_err()); } + #[test] + fn unsigned_manifest_rejected_when_required() { + let meta = ManifestMeta { + signatures_required: true, + signature_valid: false, + }; + let result = + load_manifest_with_meta(&ctx(ShellClass::System, true), &manifest_json(), &meta); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("not signed")); + } + + #[test] + fn signed_manifest_accepted() { + let meta = ManifestMeta { + signatures_required: true, + signature_valid: true, + }; + let result = + load_manifest_with_meta(&ctx(ShellClass::System, true), &manifest_json(), &meta); + assert!(result.is_ok()); + } + #[test] fn manifest_cid_is_computed() { let result = load_manifest(&ctx(ShellClass::System, true), &manifest_json()).unwrap();