use ed25519_dalek::{Signer, SigningKey};
use serde::{Deserialize, Serialize};
use crate::core::policy::{self, PolicyError, PolicyPack, ResolvedPolicy};
pub const SCHEMA_VERSION: u32 = 1;
pub const KIND: &str = "lean-ctx.org-policy";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OrgVerifyResult {
pub signature_valid: bool,
pub signer_public_key: Option<String>,
pub error: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OrgPolicyV1 {
pub schema_version: u32,
pub kind: String,
pub org: String,
pub policy_version: String,
pub issued_at: String,
pub enforced: bool,
pub pack_toml: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub signer_public_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub signature: Option<String>,
}
impl OrgPolicyV1 {
pub fn build(
org: &str,
policy_version: &str,
enforced: bool,
pack_toml: &str,
) -> Result<Self, PolicyError> {
let pack = policy::parse(pack_toml)?;
policy::resolve(&pack)?;
Ok(Self {
schema_version: SCHEMA_VERSION,
kind: KIND.to_string(),
org: org.to_string(),
policy_version: policy_version.to_string(),
issued_at: chrono::Utc::now().to_rfc3339(),
enforced,
pack_toml: pack_toml.to_string(),
signer_public_key: None,
signature: None,
})
}
pub fn pack(&self) -> Result<PolicyPack, PolicyError> {
policy::parse(&self.pack_toml)
}
pub fn resolved(&self) -> Result<ResolvedPolicy, PolicyError> {
policy::resolve(&self.pack()?)
}
pub fn canonical_bytes(&self) -> Result<Vec<u8>, String> {
let mut clone = self.clone();
clone.signature = None;
clone.signer_public_key = None;
serde_json::to_vec(&clone).map_err(|e| format!("serialize for signing: {e}"))
}
pub fn sign(&mut self) -> Result<(), String> {
let key =
crate::core::agent_identity::get_or_create_keypair(&super::org_key_id(&self.org))?;
self.sign_with_key(&key);
Ok(())
}
pub fn sign_with_key(&mut self, key: &SigningKey) {
self.signature = None;
self.signer_public_key = None;
let canonical = self.canonical_bytes().unwrap_or_default();
let sig = key.sign(&canonical);
self.signer_public_key = Some(crate::core::agent_identity::hex_encode(
&key.verifying_key().to_bytes(),
));
self.signature = Some(crate::core::agent_identity::hex_encode(&sig.to_bytes()));
}
#[must_use]
pub fn verify(&self) -> OrgVerifyResult {
let fail = |msg: &str| OrgVerifyResult {
signature_valid: false,
signer_public_key: self.signer_public_key.clone(),
error: Some(msg.to_string()),
};
if self.kind != KIND {
return fail("not an org-policy artifact");
}
let (Some(sig_hex), Some(pk_hex)) = (&self.signature, &self.signer_public_key) else {
return fail("artifact is not signed");
};
let (Ok(sig_bytes), Ok(pk_bytes)) = (
crate::core::agent_identity::hex_decode(sig_hex),
crate::core::agent_identity::hex_decode(pk_hex),
) else {
return fail("malformed signature or public key hex");
};
let canonical = match self.canonical_bytes() {
Ok(c) => c,
Err(e) => return fail(&e),
};
if crate::core::agent_identity::verify_signature(&pk_bytes, &canonical, &sig_bytes) {
OrgVerifyResult {
signature_valid: true,
signer_public_key: Some(pk_hex.clone()),
error: None,
}
} else {
fail("signature does not match payload (tampered or wrong key)")
}
}
pub fn to_json(&self) -> Result<String, String> {
serde_json::to_string_pretty(self).map_err(|e| format!("serialize org policy: {e}"))
}
pub fn from_json(text: &str) -> Result<Self, String> {
let parsed: Self = serde_json::from_str(text)
.map_err(|e| format!("not a valid org-policy artifact: {e}"))?;
if parsed.kind != KIND {
return Err(format!(
"wrong artifact kind '{}' (expected '{KIND}')",
parsed.kind
));
}
Ok(parsed)
}
}
#[cfg(test)]
mod tests {
use super::*;
const PACK: &str = r#"
name = "acme-floor"
version = "1.0.0"
description = "ACME org floor"
extends = "strict-redaction"
[context]
deny_tools = ["ctx_url_read"]
"#;
fn key() -> SigningKey {
let mut seed = [0u8; 32];
getrandom::fill(&mut seed).unwrap();
SigningKey::from_bytes(&seed)
}
#[test]
fn build_rejects_invalid_pack() {
let err = OrgPolicyV1::build("acme", "1", true, "not = valid = toml");
assert!(err.is_err());
}
#[test]
fn sign_then_verify_roundtrips() {
let mut a = OrgPolicyV1::build("acme", "2026.06.1", true, PACK).unwrap();
a.sign_with_key(&key());
assert!(a.verify().signature_valid);
}
#[test]
fn verify_detects_tampered_pack_body() {
let mut a = OrgPolicyV1::build("acme", "1", true, PACK).unwrap();
a.sign_with_key(&key());
a.pack_toml = a.pack_toml.replace("ctx_url_read", "ctx_read");
assert!(
!a.verify().signature_valid,
"editing the pack body must break the signature"
);
}
#[test]
fn verify_detects_flipped_enforced_flag() {
let mut a = OrgPolicyV1::build("acme", "1", true, PACK).unwrap();
a.sign_with_key(&key());
a.enforced = false;
assert!(!a.verify().signature_valid);
}
#[test]
fn json_roundtrip_preserves_and_verifies() {
let mut a = OrgPolicyV1::build("acme", "1", true, PACK).unwrap();
a.sign_with_key(&key());
let json = a.to_json().unwrap();
let loaded = OrgPolicyV1::from_json(&json).unwrap();
assert_eq!(loaded, a);
assert!(loaded.verify().signature_valid);
}
#[test]
fn from_json_rejects_foreign_kind() {
let json = r#"{"schema_version":1,"kind":"something-else","org":"x","policy_version":"1","issued_at":"t","enforced":false,"pack_toml":""}"#;
assert!(OrgPolicyV1::from_json(json).is_err());
}
#[test]
fn resolved_folds_extends_chain() {
let a = OrgPolicyV1::build("acme", "1", true, PACK).unwrap();
let r = a.resolved().unwrap();
assert!(r.deny_tools.contains(&"ctx_url_read".to_string()));
assert!(!r.redaction.is_empty());
}
}