Skip to main content

tf_types/
permission.rs

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