1use base64::engine::general_purpose::STANDARD as B64;
17use base64::Engine;
18use ed25519_dalek::{Signature, Verifier, VerifyingKey};
19use serde::{Deserialize, Serialize};
20
21pub const POLICY_FORMAT_VERSION: u32 = 1;
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct PolicyBundle {
27 pub policy_format_version: u32,
29 pub alg: String,
31 pub key_id: String,
33 pub issuer: String,
35 pub payload: PolicyPayload,
36 pub signature: String,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct PolicyPayload {
43 pub org_id: String,
44 pub bundle_id: String,
45 pub issued_at: String,
47 pub rules: Vec<PolicyRule>,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct PolicyRule {
54 pub id: String,
55 pub target: String,
57 pub level: String,
59 pub reason: String,
60}
61
62#[derive(Debug, Clone)]
64pub struct TrustedKey {
65 pub key_id: String,
66 pub public_key: [u8; 32],
67}
68
69pub fn default_trusted_keys() -> Vec<TrustedKey> {
74 Vec::new()
75}
76
77#[derive(Debug, thiserror::Error, PartialEq, Eq)]
78pub enum PolicyError {
79 #[error("unsupported policy_format_version {0} (expected {POLICY_FORMAT_VERSION})")]
80 UnsupportedVersion(u32),
81 #[error("unsupported signature algorithm {0:?} (expected \"ed25519\")")]
82 UnsupportedAlg(String),
83 #[error("no trusted key matches key_id {0:?}")]
84 UnknownKey(String),
85 #[error("policy bundle signature verification failed (corrupt, tampered, or wrong key)")]
86 SignatureInvalid,
87}
88
89#[derive(Debug, Clone)]
92pub struct VerifiedBundle {
93 pub org_id: String,
94 pub bundle_id: String,
95 pub rules: Vec<PolicyRule>,
96}
97
98fn canonical_payload_bytes(payload: &PolicyPayload) -> Vec<u8> {
103 #[derive(Serialize)]
104 struct CanonicalRule<'a> {
105 id: &'a str,
106 target: &'a str,
107 level: &'a str,
108 reason: &'a str,
109 }
110 #[derive(Serialize)]
111 struct Canonical<'a> {
112 org_id: &'a str,
113 bundle_id: &'a str,
114 issued_at: &'a str,
115 rules: Vec<CanonicalRule<'a>>,
116 }
117 let canonical = Canonical {
118 org_id: &payload.org_id,
119 bundle_id: &payload.bundle_id,
120 issued_at: &payload.issued_at,
121 rules: payload
122 .rules
123 .iter()
124 .map(|r| CanonicalRule {
125 id: &r.id,
126 target: &r.target,
127 level: &r.level,
128 reason: &r.reason,
129 })
130 .collect(),
131 };
132 serde_json::to_vec(&canonical).expect("canonical serialization cannot fail")
133}
134
135pub fn verify_bundle(
142 bundle: &PolicyBundle,
143 trusted_keys: &[TrustedKey],
144) -> Result<VerifiedBundle, PolicyError> {
145 if bundle.policy_format_version != POLICY_FORMAT_VERSION {
146 return Err(PolicyError::UnsupportedVersion(
147 bundle.policy_format_version,
148 ));
149 }
150 if bundle.alg != "ed25519" {
151 return Err(PolicyError::UnsupportedAlg(bundle.alg.clone()));
152 }
153
154 let trusted = trusted_keys
155 .iter()
156 .find(|k| k.key_id == bundle.key_id)
157 .ok_or_else(|| PolicyError::UnknownKey(bundle.key_id.clone()))?;
158
159 let verifying =
160 VerifyingKey::from_bytes(&trusted.public_key).map_err(|_| PolicyError::SignatureInvalid)?;
161
162 let sig_bytes = B64
163 .decode(bundle.signature.as_bytes())
164 .map_err(|_| PolicyError::SignatureInvalid)?;
165 let sig_arr: [u8; 64] = sig_bytes
166 .as_slice()
167 .try_into()
168 .map_err(|_| PolicyError::SignatureInvalid)?;
169 let signature = Signature::from_bytes(&sig_arr);
170
171 let canonical = canonical_payload_bytes(&bundle.payload);
172 verifying
173 .verify(&canonical, &signature)
174 .map_err(|_| PolicyError::SignatureInvalid)?;
175
176 Ok(VerifiedBundle {
177 org_id: bundle.payload.org_id.clone(),
178 bundle_id: bundle.payload.bundle_id.clone(),
179 rules: bundle.payload.rules.clone(),
180 })
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use ed25519_dalek::{Signer, SigningKey};
187
188 const TEST_SEED: [u8; 32] = [7u8; 32];
189
190 fn test_key() -> (SigningKey, TrustedKey) {
191 let sk = SigningKey::from_bytes(&TEST_SEED);
192 let public_key = sk.verifying_key().to_bytes();
193 (
194 sk,
195 TrustedKey {
196 key_id: "test-key-1".into(),
197 public_key,
198 },
199 )
200 }
201
202 fn sample_payload() -> PolicyPayload {
203 PolicyPayload {
204 org_id: "acme".into(),
205 bundle_id: "b-001".into(),
206 issued_at: "2026-06-28T00:00:00Z".into(),
207 rules: vec![PolicyRule {
208 id: "PHI-1".into(),
209 target: "src/payments/**".into(),
210 level: "deny".into(),
211 reason: "PHI files require consultation".into(),
212 }],
213 }
214 }
215
216 fn sign_with(sk: &SigningKey, key_id: &str, payload: PolicyPayload) -> PolicyBundle {
217 let sig = sk.sign(&canonical_payload_bytes(&payload));
218 PolicyBundle {
219 policy_format_version: POLICY_FORMAT_VERSION,
220 alg: "ed25519".into(),
221 key_id: key_id.to_string(),
222 issuer: "acme".into(),
223 payload,
224 signature: B64.encode(sig.to_bytes()),
225 }
226 }
227
228 #[test]
229 fn accepts_a_correctly_signed_bundle() {
230 let (sk, trusted) = test_key();
231 let bundle = sign_with(&sk, &trusted.key_id, sample_payload());
232 let verified = verify_bundle(&bundle, &[trusted]).expect("should verify");
233 assert_eq!(verified.org_id, "acme");
234 assert_eq!(verified.rules.len(), 1);
235 assert_eq!(verified.rules[0].id, "PHI-1");
236 assert_eq!(verified.rules[0].level, "deny");
237 }
238
239 #[test]
240 fn rejects_a_tampered_payload() {
241 let (sk, trusted) = test_key();
243 let mut bundle = sign_with(&sk, &trusted.key_id, sample_payload());
244 bundle.payload.rules[0].level = "advisory".into(); assert!(matches!(
246 verify_bundle(&bundle, &[trusted]),
247 Err(PolicyError::SignatureInvalid)
248 ));
249 }
250
251 #[test]
252 fn rejects_an_unknown_key_id() {
253 let (sk, trusted) = test_key();
254 let bundle = sign_with(&sk, "some-other-key", sample_payload());
255 assert!(matches!(
256 verify_bundle(&bundle, &[trusted]),
257 Err(PolicyError::UnknownKey(_))
258 ));
259 }
260
261 #[test]
262 fn rejects_a_signature_from_an_untrusted_key() {
263 let (_sk, trusted) = test_key();
265 let attacker = SigningKey::from_bytes(&[9u8; 32]);
266 let payload = sample_payload();
267 let sig = attacker.sign(&canonical_payload_bytes(&payload));
268 let bundle = PolicyBundle {
269 policy_format_version: POLICY_FORMAT_VERSION,
270 alg: "ed25519".into(),
271 key_id: trusted.key_id.clone(),
272 issuer: "acme".into(),
273 payload,
274 signature: B64.encode(sig.to_bytes()),
275 };
276 assert!(matches!(
277 verify_bundle(&bundle, &[trusted]),
278 Err(PolicyError::SignatureInvalid)
279 ));
280 }
281
282 #[test]
283 fn rejects_bad_version_and_alg() {
284 let (sk, trusted) = test_key();
285 let mut v = sign_with(&sk, &trusted.key_id, sample_payload());
286 v.policy_format_version = 999;
287 assert!(matches!(
288 verify_bundle(&v, std::slice::from_ref(&trusted)),
289 Err(PolicyError::UnsupportedVersion(999))
290 ));
291
292 let mut a = sign_with(&sk, &trusted.key_id, sample_payload());
293 a.alg = "rsa".into();
294 assert!(matches!(
295 verify_bundle(&a, &[trusted]),
296 Err(PolicyError::UnsupportedAlg(_))
297 ));
298 }
299
300 #[test]
301 fn oss_core_trusts_no_keys_so_floor_is_dormant() {
302 let (sk, trusted) = test_key();
306 let bundle = sign_with(&sk, &trusted.key_id, sample_payload());
307 assert!(matches!(
308 verify_bundle(&bundle, &default_trusted_keys()),
309 Err(PolicyError::UnknownKey(_))
310 ));
311 }
312}