1use base64::engine::general_purpose::STANDARD;
5use base64::Engine;
6use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use sha2::{Digest, Sha256};
10
11use crate::canonicalize;
12use crate::expiration::{is_within_window, Window};
13
14#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
15pub struct PermissionRequest {
16 pub request_version: String,
17 pub id: String,
18 pub agent: String,
19 #[serde(skip_serializing_if = "Option::is_none", default)]
20 pub instance: Option<String>,
21 #[serde(skip_serializing_if = "Option::is_none", default)]
22 pub human: Option<String>,
23 #[serde(skip_serializing_if = "Option::is_none", default)]
24 pub model: Option<String>,
25 #[serde(skip_serializing_if = "Option::is_none", default)]
26 pub tool: Option<String>,
27 pub action: String,
28 #[serde(skip_serializing_if = "Option::is_none", default)]
29 pub target: Option<String>,
30 #[serde(skip_serializing_if = "Option::is_none", default)]
31 pub risk: Option<String>,
32 #[serde(skip_serializing_if = "Option::is_none", default)]
33 pub danger_tags: Option<Vec<String>>,
34 #[serde(skip_serializing_if = "Option::is_none", default)]
35 pub duration_seconds: Option<u64>,
36 pub reason: String,
37 #[serde(skip_serializing_if = "Option::is_none", default)]
38 pub proof_level_offered: Option<String>,
39 pub requested_at: String,
40 #[serde(skip_serializing_if = "Option::is_none", default)]
41 pub context: Option<Value>,
42}
43
44#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
45pub struct PermissionGrant {
46 pub grant_version: String,
47 pub request_id: String,
48 pub decision: String,
49 #[serde(skip_serializing_if = "Option::is_none", default)]
50 pub capability: Option<Value>,
51 #[serde(skip_serializing_if = "Option::is_none", default)]
52 pub constraints: Option<Vec<Value>>,
53 #[serde(skip_serializing_if = "Option::is_none", default)]
54 pub policy_decision: Option<Value>,
55 #[serde(skip_serializing_if = "Option::is_none", default)]
56 pub ceremony_id: Option<String>,
57 #[serde(skip_serializing_if = "Option::is_none", default)]
58 pub denial_reason: Option<String>,
59 #[serde(skip_serializing_if = "Option::is_none", default)]
60 pub valid_from: Option<String>,
61 #[serde(skip_serializing_if = "Option::is_none", default)]
62 pub valid_until: Option<String>,
63 pub issued_at: String,
64 pub issuer: String,
65 pub signature: SignatureEnvelope,
66}
67
68#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
69pub struct SignatureEnvelope {
70 pub algorithm: String,
71 pub signer: String,
72 pub signature: String,
73}
74
75#[derive(Debug)]
76pub struct VerifyPermissionGrantResult {
77 pub ok: bool,
78 pub reason: Option<String>,
79}
80
81pub fn permission_grant_signing_bytes(grant: &PermissionGrant) -> [u8; 32] {
82 let mut value = serde_json::to_value(grant).unwrap_or(Value::Null);
83 if let Value::Object(map) = &mut value {
84 map.remove("signature");
85 }
86 let canonical = canonicalize(&value).unwrap_or_default();
87 Sha256::digest(canonical.as_bytes()).into()
88}
89
90#[allow(clippy::too_many_arguments)]
91pub fn sign_permission_grant(
92 request: &PermissionRequest,
93 decision: &str,
94 issuer: &str,
95 private_key: &[u8; 32],
96 capability: Option<Value>,
97 constraints: Option<Vec<Value>>,
98 policy_decision: Option<Value>,
99 ceremony_id: Option<String>,
100 denial_reason: Option<String>,
101 issued_at: Option<String>,
102 valid_from: Option<String>,
103 valid_until: Option<String>,
104) -> PermissionGrant {
105 let issued_at = issued_at.unwrap_or_else(now_iso8601);
106 let mut grant = PermissionGrant {
107 grant_version: "1".into(),
108 request_id: request.id.clone(),
109 decision: decision.into(),
110 capability,
111 constraints: constraints.filter(|c| !c.is_empty()),
112 policy_decision,
113 ceremony_id,
114 denial_reason,
115 valid_from,
116 valid_until,
117 issued_at,
118 issuer: issuer.into(),
119 signature: SignatureEnvelope {
120 algorithm: "ed25519".into(),
121 signer: issuer.into(),
122 signature: String::new(),
123 },
124 };
125 let digest = permission_grant_signing_bytes(&grant);
126 let signing = SigningKey::from_bytes(private_key);
127 let sig: Signature = signing.sign(&digest);
128 grant.signature.signature = STANDARD.encode(sig.to_bytes());
129 grant
130}
131
132pub fn verify_permission_grant(
133 grant: &PermissionGrant,
134 public_key: &[u8; 32],
135 request: Option<&PermissionRequest>,
136 now: Option<&str>,
137) -> VerifyPermissionGrantResult {
138 if grant.grant_version != "1" {
139 return rejected(format!("unsupported grant_version {}", grant.grant_version));
140 }
141 if grant.signature.signer != grant.issuer {
142 return rejected("signature signer does not match issuer".into());
143 }
144 if grant.signature.algorithm != "ed25519" {
145 return rejected(format!(
146 "unsupported signature algorithm {}",
147 grant.signature.algorithm
148 ));
149 }
150 if let Some(req) = request {
151 if grant.request_id != req.id {
152 return rejected("grant.request_id does not match request.id".into());
153 }
154 }
155 let now_string = now.map(str::to_string).unwrap_or_else(now_iso8601);
156 let window = Window {
157 valid_from: grant.valid_from.as_deref(),
158 valid_until: grant.valid_until.as_deref(),
159 ..Window::default()
160 };
161 if !is_within_window(&window, &now_string) {
162 return rejected("grant outside valid_from/valid_until window".into());
163 }
164 let digest = permission_grant_signing_bytes(grant);
165 let sig_bytes = match STANDARD.decode(&grant.signature.signature) {
166 Ok(b) => b,
167 Err(e) => return rejected(format!("signature base64 decode: {}", e)),
168 };
169 let sig = match Signature::from_slice(&sig_bytes) {
170 Ok(s) => s,
171 Err(e) => return rejected(format!("signature parse: {}", e)),
172 };
173 let vk = match VerifyingKey::from_bytes(public_key) {
174 Ok(v) => v,
175 Err(e) => return rejected(format!("verifying key: {}", e)),
176 };
177 if vk.verify(&digest, &sig).is_err() {
178 return rejected("grant signature did not verify".into());
179 }
180 VerifyPermissionGrantResult {
181 ok: true,
182 reason: None,
183 }
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize, Default)]
187pub struct Provenance {
188 #[serde(skip_serializing_if = "Option::is_none", default)]
189 pub human: Option<String>,
190 #[serde(skip_serializing_if = "Option::is_none", default)]
191 pub agent: Option<String>,
192 #[serde(skip_serializing_if = "Option::is_none", default)]
193 pub instance: Option<String>,
194 #[serde(skip_serializing_if = "Option::is_none", default)]
195 pub model: Option<String>,
196 #[serde(skip_serializing_if = "Option::is_none", default)]
197 pub tool: Option<String>,
198 #[serde(skip_serializing_if = "Option::is_none", default)]
199 pub requested_action: Option<String>,
200}
201
202pub fn provenance_from_request(req: &PermissionRequest) -> Provenance {
203 Provenance {
204 human: req.human.clone(),
205 agent: Some(req.agent.clone()),
206 instance: req.instance.clone(),
207 model: req.model.clone(),
208 tool: req.tool.clone(),
209 requested_action: Some(req.action.clone()),
210 }
211}
212
213fn rejected(reason: String) -> VerifyPermissionGrantResult {
214 VerifyPermissionGrantResult {
215 ok: false,
216 reason: Some(reason),
217 }
218}
219
220fn now_iso8601() -> String {
221 let secs = std::time::SystemTime::now()
222 .duration_since(std::time::UNIX_EPOCH)
223 .unwrap_or_default()
224 .as_secs() as i64;
225 let (y, m, d, h, mi, s) = secs_to_ymdhms(secs);
226 format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", y, m, d, h, mi, s)
227}
228
229fn secs_to_ymdhms(secs: i64) -> (i32, u32, u32, u32, u32, u32) {
230 let days = secs.div_euclid(86_400);
231 let time = secs.rem_euclid(86_400);
232 let hour = (time / 3600) as u32;
233 let minute = ((time % 3600) / 60) as u32;
234 let second = (time % 60) as u32;
235 let z = days + 719_468;
236 let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
237 let doe = (z - era * 146_097) as u64;
238 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
239 let y = yoe as i64 + era * 400;
240 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
241 let mp = (5 * doy + 2) / 153;
242 let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
243 let m = if mp < 10 {
244 (mp + 3) as u32
245 } else {
246 (mp - 9) as u32
247 };
248 let year = if m <= 2 { y + 1 } else { y };
249 (year as i32, m, d, hour, minute, second)
250}