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 <tking@guildhouse.dev>
Signed-off-by: Tyler J King <tking727@gmail.com>
This commit is contained in:
Tyler J King 2026-04-15 19:57:48 -04:00
parent 7380b834d1
commit 8cec5a6486

View file

@ -52,10 +52,41 @@ pub struct ManifestLoadResult {
/// Reads the manifest JSON (from ConfigMap mount or API), filters /// Reads the manifest JSON (from ConfigMap mount or API), filters
/// entries by the session's ShellClass and delegation scope, and /// entries by the session's ShellClass and delegation scope, and
/// optionally verifies on-disk binary hashes. /// 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( pub fn load_manifest(
ctx: &SessionContext, ctx: &SessionContext,
manifest_json: &str, manifest_json: &str,
) -> Result<ManifestLoadResult, String> { ) -> Result<ManifestLoadResult, String> {
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<ManifestLoadResult, String> {
// 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<VerifiedEntry> = let entries: Vec<VerifiedEntry> =
serde_json::from_str(manifest_json).map_err(|e| format!("manifest parse error: {e}"))?; serde_json::from_str(manifest_json).map_err(|e| format!("manifest parse error: {e}"))?;
@ -248,6 +279,29 @@ mod tests {
assert!(result.is_err()); 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] #[test]
fn manifest_cid_is_computed() { fn manifest_cid_is_computed() {
let result = load_manifest(&ctx(ShellClass::System, true), &manifest_json()).unwrap(); let result = load_manifest(&ctx(ShellClass::System, true), &manifest_json()).unwrap();