use ed25519_dalek::SigningKey;
use serde::{Deserialize, Serialize};
use crate::core::agent_identity::{hex_decode, hex_encode, sign_bytes_with, verify_signature};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RegistrySignature {
pub signature: String,
pub signer_public_key: String,
}
impl RegistrySignature {
pub fn from_json(text: &str) -> Result<Self, String> {
serde_json::from_str(text).map_err(|e| format!("not a valid registry signature: {e}"))
}
pub fn to_json(&self) -> Result<String, String> {
serde_json::to_string_pretty(self).map_err(|e| format!("serialize signature: {e}"))
}
}
#[must_use]
pub fn sidecar_path(registry_path: &std::path::Path) -> std::path::PathBuf {
let mut s = registry_path.as_os_str().to_os_string();
s.push(".sig");
std::path::PathBuf::from(s)
}
#[must_use]
pub fn sign_detached(content: &str, key: &SigningKey) -> RegistrySignature {
let sig = sign_bytes_with(key, content.as_bytes());
RegistrySignature {
signature: hex_encode(&sig),
signer_public_key: hex_encode(&key.verifying_key().to_bytes()),
}
}
#[must_use]
pub fn signature_valid(content: &str, sig: &RegistrySignature) -> bool {
let (Ok(pk), Ok(s)) = (
hex_decode(&sig.signer_public_key),
hex_decode(&sig.signature),
) else {
return false;
};
verify_signature(&pk, content.as_bytes(), &s)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OverrideVerdict {
Accept,
Reject(String),
}
#[must_use]
pub fn gate_override(
content: &str,
sig: Option<&RegistrySignature>,
require_signature: bool,
is_trusted: impl Fn(&str) -> bool,
) -> OverrideVerdict {
if !require_signature {
return OverrideVerdict::Accept;
}
let Some(sig) = sig else {
return OverrideVerdict::Reject(
"addons.require_signature is on but the override registry has no .sig sidecar"
.to_string(),
);
};
if !signature_valid(content, sig) {
return OverrideVerdict::Reject(
"override registry signature is invalid (tampered or wrong key)".to_string(),
);
}
if !is_trusted(&sig.signer_public_key) {
return OverrideVerdict::Reject(format!(
"override registry signed by an untrusted key ({}…) — pin it with `policy org trust`",
sig.signer_public_key.chars().take(12).collect::<String>()
));
}
OverrideVerdict::Accept
}
#[cfg(test)]
mod tests {
use super::*;
fn key() -> SigningKey {
SigningKey::from_bytes(&[7u8; 32])
}
#[test]
fn sign_then_verify_roundtrips() {
let k = key();
let content = "{\"addons\":[]}";
let sig = sign_detached(content, &k);
assert!(signature_valid(content, &sig));
}
#[test]
fn tampered_content_fails() {
let sig = sign_detached("original", &key());
assert!(!signature_valid("tampered", &sig));
}
#[test]
fn sidecar_path_appends_sig() {
let p = sidecar_path(std::path::Path::new("/x/addon_registry.json"));
assert_eq!(p, std::path::PathBuf::from("/x/addon_registry.json.sig"));
}
#[test]
fn gate_accepts_when_not_required() {
assert_eq!(
gate_override("anything", None, false, |_| false),
OverrideVerdict::Accept
);
}
#[test]
fn gate_rejects_missing_sidecar() {
assert!(matches!(
gate_override("x", None, true, |_| true),
OverrideVerdict::Reject(_)
));
}
#[test]
fn gate_rejects_invalid_signature() {
let bad = RegistrySignature {
signature: "00".repeat(64),
signer_public_key: hex_encode(&key().verifying_key().to_bytes()),
};
assert!(matches!(
gate_override("content", Some(&bad), true, |_| true),
OverrideVerdict::Reject(_)
));
}
#[test]
fn gate_rejects_untrusted_signer() {
let content = "content";
let sig = sign_detached(content, &key());
assert!(matches!(
gate_override(content, Some(&sig), true, |_| false),
OverrideVerdict::Reject(_)
));
}
#[test]
fn gate_accepts_signed_and_trusted() {
let content = "content";
let sig = sign_detached(content, &key());
let trusted_pk = sig.signer_public_key.clone();
assert_eq!(
gate_override(content, Some(&sig), true, |pk| pk == trusted_pk),
OverrideVerdict::Accept
);
}
}