1use base64::{Engine, engine::general_purpose::STANDARD as B64};
34use ijima_core::{IjimaError, Result};
35use schubert::{
36 AccessController, AccessDecision, PrincipalId,
37 crypto::{CapabilityIssuer, CapabilityToken, CapabilityVerifier},
38};
39
40const POLICY_TOML: &str = include_str!("../policy/policy.toml");
42
43const ISSUER_KEY_LEN: usize = 32;
44const SIGNATURE_LEN: usize = 64;
45
46#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct AuthenticatedPrincipal {
53 pub principal: PrincipalId,
55 pub capability: String,
57}
58
59impl AuthenticatedPrincipal {
60 pub fn may(&self, required: &str) -> bool {
63 self.capability == required || self.capability == ijima_core::capabilities::ADMIN
64 }
65
66 pub fn personal_namespace(&self) -> ijima_core::NamespaceId {
70 ijima_core::NamespaceId::new(format!("ns_{}_private", self.principal.as_str()))
71 }
72}
73
74#[derive(Debug)]
80pub struct IjimaAuth {
81 controller: AccessController,
82 issuer: CapabilityIssuer,
83 verifier: CapabilityVerifier,
84}
85
86impl IjimaAuth {
87 pub fn from_embedded_policy() -> Result<Self> {
100 Self::from_embedded_policy_with_seed(Self::generate_seed())
101 }
102
103 pub fn from_embedded_policy_with_seed(seed: [u8; 32]) -> Result<Self> {
111 let controller = AccessController::from_policy_toml(POLICY_TOML)
112 .map_err(|e| IjimaError::invalid_input(format!("policy load: {e}")))?;
113 let issuer = CapabilityIssuer::from_seed(seed);
114 let verifier = CapabilityVerifier::new(issuer.public_key());
115 Ok(Self {
116 controller,
117 issuer,
118 verifier,
119 })
120 }
121
122 pub fn generate_seed() -> [u8; 32] {
124 use rand::TryRngCore;
125 let mut seed = [0u8; 32];
126 rand::rngs::OsRng
127 .try_fill_bytes(&mut seed)
128 .expect("OsRng is infallible in practice");
129 seed
130 }
131
132 pub fn issuer_public_key_hex(&self) -> String {
135 self.issuer
136 .public_key()
137 .iter()
138 .map(|b| format!("{b:02x}"))
139 .collect()
140 }
141
142 pub fn grassmannian(&self) -> (usize, usize) {
144 self.controller.grassmannian()
145 }
146
147 pub fn issue_bearer(
155 &self,
156 principal: impl Into<PrincipalId>,
157 capability: impl AsRef<str>,
158 ) -> Result<String> {
159 let capability_str = capability.as_ref();
160 let token = self
161 .issuer
162 .issue(principal, capability_str)
163 .map_err(|e| IjimaError::invalid_input(format!("token issue: {e}")))?;
164 encode_token(&token)
165 }
166
167 pub fn verify_bearer(&self, bearer: &str) -> Result<AuthenticatedPrincipal> {
175 let token = decode_token(bearer)?;
176 let (principal, capability) = self
177 .verifier
178 .verify_and_extract(&token)
179 .map_err(|e| IjimaError::invalid_input(format!("token verify: {e}")))?;
180 Ok(AuthenticatedPrincipal {
181 principal: principal.clone(),
182 capability: capability.as_str().to_string(),
183 })
184 }
185
186 pub fn check(&self, principal: &PrincipalId, required: &[&str]) -> Result<AccessDecision> {
192 self.controller
193 .check(principal, required)
194 .map_err(|e| IjimaError::invalid_input(format!("access check: {e}")))
195 }
196
197 pub fn require(&self, bearer: &str, required: &str) -> Result<AuthenticatedPrincipal> {
213 let principal = self.verify_bearer(bearer)?;
214 if principal.capability == required
215 || principal.capability == ijima_core::capabilities::ADMIN
216 {
217 Ok(principal)
218 } else {
219 Err(IjimaError::invalid_input(format!(
220 "access denied: token grants '{}' but '{}' is required",
221 principal.capability, required
222 )))
223 }
224 }
225}
226
227fn encode_token(token: &CapabilityToken) -> Result<String> {
230 let p = token.principal.as_str().as_bytes();
231 let c = token.capability.as_str().as_bytes();
232 if p.len() > u16::MAX as usize || c.len() > u16::MAX as usize {
233 return Err(IjimaError::invalid_input("token field too long"));
234 }
235 let mut buf = Vec::with_capacity(2 + p.len() + 2 + c.len() + ISSUER_KEY_LEN + SIGNATURE_LEN);
236 buf.extend_from_slice(&(p.len() as u16).to_be_bytes());
237 buf.extend_from_slice(p);
238 buf.extend_from_slice(&(c.len() as u16).to_be_bytes());
239 buf.extend_from_slice(c);
240 if token.issuer_key.len() != ISSUER_KEY_LEN || token.signature.len() != SIGNATURE_LEN {
241 return Err(IjimaError::invalid_input(
242 "malformed issuer key or signature",
243 ));
244 }
245 buf.extend_from_slice(&token.issuer_key);
246 buf.extend_from_slice(&token.signature);
247 Ok(B64.encode(&buf))
248}
249
250fn decode_token(bearer: &str) -> Result<CapabilityToken> {
251 let buf = B64
252 .decode(bearer.trim())
253 .map_err(|e| IjimaError::invalid_input(format!("base64 decode: {e}")))?;
254 let mut pos = 0;
255 let plen = read_u16(&buf, &mut pos)?;
256 let principal = read_str(&buf, &mut pos, plen)?;
257 let clen = read_u16(&buf, &mut pos)?;
258 let capability = read_str(&buf, &mut pos, clen)?;
259 let issuer_key = read_bytes(&buf, &mut pos, ISSUER_KEY_LEN)?;
260 let signature = read_bytes(&buf, &mut pos, SIGNATURE_LEN)?;
261 if pos != buf.len() {
262 return Err(IjimaError::invalid_input("trailing bytes in token"));
263 }
264 Ok(CapabilityToken {
265 principal: PrincipalId::new(principal),
266 capability: schubert::CapabilityId::new(capability),
267 issuer_key: issuer_key.to_vec(),
268 signature: signature.to_vec(),
269 })
270}
271
272fn read_u16(buf: &[u8], pos: &mut usize) -> Result<usize> {
273 if *pos + 2 > buf.len() {
274 return Err(IjimaError::invalid_input("truncated token length"));
275 }
276 let v = u16::from_be_bytes([buf[*pos], buf[*pos + 1]]) as usize;
277 *pos += 2;
278 Ok(v)
279}
280
281fn read_str(buf: &[u8], pos: &mut usize, len: usize) -> Result<String> {
282 let bytes = read_bytes(buf, pos, len)?;
283 String::from_utf8(bytes.to_vec())
284 .map_err(|e| IjimaError::invalid_input(format!("non-utf8 token field: {e}")))
285}
286
287fn read_bytes<'a>(buf: &'a [u8], pos: &mut usize, len: usize) -> Result<&'a [u8]> {
288 if *pos + len > buf.len() {
289 return Err(IjimaError::invalid_input("truncated token field"));
290 }
291 let slice = &buf[*pos..*pos + len];
292 *pos += len;
293 Ok(slice)
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299 use ijima_core::capabilities::{ADMIN, MEMORY_READ, MEMORY_WRITE};
300
301 fn fresh() -> IjimaAuth {
302 IjimaAuth::from_embedded_policy().expect("embedded policy must load")
303 }
304
305 #[test]
306 fn embedded_policy_loads_on_gr_4_8() {
307 let auth = fresh();
308 assert_eq!(auth.grassmannian(), (4, 8));
309 }
310
311 #[test]
312 fn issue_then_verify_round_trips() {
313 let auth = fresh();
314 let bearer = auth
315 .issue_bearer("elliott", MEMORY_READ)
316 .expect("must issue");
317 let principal = auth.verify_bearer(&bearer).expect("must verify");
318 assert_eq!(principal.principal.as_str(), "elliott");
319 assert_eq!(principal.capability, MEMORY_READ);
320 }
321
322 #[test]
323 fn tampered_signature_is_rejected() {
324 let auth = fresh();
325 let mut buf = B64
326 .decode(
327 auth.issue_bearer("elliott", MEMORY_READ)
328 .expect("must issue"),
329 )
330 .unwrap();
331 let last = buf.len() - 1;
333 buf[last] ^= 0xff;
334 let tampered = B64.encode(&buf);
335 assert!(auth.verify_bearer(&tampered).is_err());
336 }
337
338 #[test]
339 fn admin_token_grants_any_capability() {
340 let auth = fresh();
343 let bearer = auth.issue_bearer("root", ADMIN).expect("must issue");
344 let principal = auth.require(&bearer, MEMORY_READ).expect("admin may read");
345 assert_eq!(principal.principal.as_str(), "root");
346 }
347
348 #[test]
349 fn read_token_does_not_grant_write() {
350 let auth = fresh();
351 let bearer = auth.issue_bearer("alice", MEMORY_READ).expect("must issue");
352 assert!(auth.require(&bearer, MEMORY_WRITE).is_err());
356 }
357
358 #[test]
359 fn malformed_bearer_rejected() {
360 let auth = fresh();
361 assert!(auth.verify_bearer("not-base64!!!").is_err());
362 assert!(auth.verify_bearer("").is_err());
363 }
364
365 #[test]
366 fn seed_based_issue_then_verify_across_instances() {
367 let seed = IjimaAuth::generate_seed();
371 let issuer = IjimaAuth::from_embedded_policy_with_seed(seed).expect("issuer");
372 let bearer = issuer
373 .issue_bearer("elliott", MEMORY_READ)
374 .expect("must issue");
375 let public_key = issuer.issuer_public_key_hex();
376 assert_eq!(public_key.len(), 64);
377
378 let daemon = IjimaAuth::from_embedded_policy_with_seed(seed).expect("daemon");
380 let principal = daemon.verify_bearer(&bearer).expect("must verify");
381 assert_eq!(principal.principal.as_str(), "elliott");
382 assert_eq!(principal.capability, MEMORY_READ);
383 assert_eq!(daemon.issuer_public_key_hex(), public_key);
385 }
386}