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 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}