lean_ctx/core/addons/
signing.rs1use ed25519_dalek::SigningKey;
16use serde::{Deserialize, Serialize};
17
18use crate::core::agent_identity::{hex_decode, hex_encode, sign_bytes_with, verify_signature};
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct RegistrySignature {
23 pub signature: String,
25 pub signer_public_key: String,
28}
29
30impl RegistrySignature {
31 pub fn from_json(text: &str) -> Result<Self, String> {
33 serde_json::from_str(text).map_err(|e| format!("not a valid registry signature: {e}"))
34 }
35
36 pub fn to_json(&self) -> Result<String, String> {
38 serde_json::to_string_pretty(self).map_err(|e| format!("serialize signature: {e}"))
39 }
40}
41
42#[must_use]
44pub fn sidecar_path(registry_path: &std::path::Path) -> std::path::PathBuf {
45 let mut s = registry_path.as_os_str().to_os_string();
46 s.push(".sig");
47 std::path::PathBuf::from(s)
48}
49
50#[must_use]
53pub fn sign_detached(content: &str, key: &SigningKey) -> RegistrySignature {
54 let sig = sign_bytes_with(key, content.as_bytes());
55 RegistrySignature {
56 signature: hex_encode(&sig),
57 signer_public_key: hex_encode(&key.verifying_key().to_bytes()),
58 }
59}
60
61#[must_use]
64pub fn signature_valid(content: &str, sig: &RegistrySignature) -> bool {
65 let (Ok(pk), Ok(s)) = (
66 hex_decode(&sig.signer_public_key),
67 hex_decode(&sig.signature),
68 ) else {
69 return false;
70 };
71 verify_signature(&pk, content.as_bytes(), &s)
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
76pub enum OverrideVerdict {
77 Accept,
79 Reject(String),
81}
82
83#[must_use]
88pub fn gate_override(
89 content: &str,
90 sig: Option<&RegistrySignature>,
91 require_signature: bool,
92 is_trusted: impl Fn(&str) -> bool,
93) -> OverrideVerdict {
94 if !require_signature {
95 return OverrideVerdict::Accept;
96 }
97 let Some(sig) = sig else {
98 return OverrideVerdict::Reject(
99 "addons.require_signature is on but the override registry has no .sig sidecar"
100 .to_string(),
101 );
102 };
103 if !signature_valid(content, sig) {
104 return OverrideVerdict::Reject(
105 "override registry signature is invalid (tampered or wrong key)".to_string(),
106 );
107 }
108 if !is_trusted(&sig.signer_public_key) {
109 return OverrideVerdict::Reject(format!(
110 "override registry signed by an untrusted key ({}…) — pin it with `policy org trust`",
111 &sig.signer_public_key.chars().take(12).collect::<String>()
112 ));
113 }
114 OverrideVerdict::Accept
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 fn key() -> SigningKey {
122 SigningKey::from_bytes(&[7u8; 32])
123 }
124
125 #[test]
126 fn sign_then_verify_roundtrips() {
127 let k = key();
128 let content = "{\"addons\":[]}";
129 let sig = sign_detached(content, &k);
130 assert!(signature_valid(content, &sig));
131 }
132
133 #[test]
134 fn tampered_content_fails() {
135 let sig = sign_detached("original", &key());
136 assert!(!signature_valid("tampered", &sig));
137 }
138
139 #[test]
140 fn sidecar_path_appends_sig() {
141 let p = sidecar_path(std::path::Path::new("/x/addon_registry.json"));
142 assert_eq!(p, std::path::PathBuf::from("/x/addon_registry.json.sig"));
143 }
144
145 #[test]
146 fn gate_accepts_when_not_required() {
147 assert_eq!(
148 gate_override("anything", None, false, |_| false),
149 OverrideVerdict::Accept
150 );
151 }
152
153 #[test]
154 fn gate_rejects_missing_sidecar() {
155 assert!(matches!(
156 gate_override("x", None, true, |_| true),
157 OverrideVerdict::Reject(_)
158 ));
159 }
160
161 #[test]
162 fn gate_rejects_invalid_signature() {
163 let bad = RegistrySignature {
164 signature: "00".repeat(64),
165 signer_public_key: hex_encode(&key().verifying_key().to_bytes()),
166 };
167 assert!(matches!(
168 gate_override("content", Some(&bad), true, |_| true),
169 OverrideVerdict::Reject(_)
170 ));
171 }
172
173 #[test]
174 fn gate_rejects_untrusted_signer() {
175 let content = "content";
176 let sig = sign_detached(content, &key());
177 assert!(matches!(
178 gate_override(content, Some(&sig), true, |_| false),
179 OverrideVerdict::Reject(_)
180 ));
181 }
182
183 #[test]
184 fn gate_accepts_signed_and_trusted() {
185 let content = "content";
186 let sig = sign_detached(content, &key());
187 let trusted_pk = sig.signer_public_key.clone();
188 assert_eq!(
189 gate_override(content, Some(&sig), true, |pk| pk == trusted_pk),
190 OverrideVerdict::Accept
191 );
192 }
193}