nx_cap_token.nx source
↩ module page · 144 lines · 8076 B
1// nx_cap_token.nx -- sovereign CAPABILITY TOKEN: the "beyond MCP" security primitive for tool invocation.
2// A capability is AUTHORITY-IN-THE-TOKEN bound to a designated tool-set -- unforgeable + attenuable. Unlike an
3// OAuth/JWT bearer (identity + AMBIENT scope, which MCP's own docs admit leads to confused-deputy), verifying a
4// capability consults NO ambient identity: the token itself names the tools AND confers the authority to call them.
5// Composes the shipped signed-token MAC (signed_cookie_sign/verify = "<value>.<b64url(HMAC-SHA256(key,value))>" +
6// constant-time verify); adds ONLY the capability semantics (allow-set membership, expiry, SUBSET-ONLY attenuation).
7// payload = "<allow>~<exp>~<nonce>" allow = comma-separated tool names or "*"; exp = decimal epoch; nonce = decimal
8// token = signed_cookie_sign(payload) = "<payload>.<sig>"
9// license_tier: ORIGINAL
10import "nx_signed_cookie.nx" // signed_cookie_sign/verify (+ transitive hmac_sha256 / nx_base64 / nx_ct / syscalls)
11
12const CAPT_OK: i64 = 1
13const CAPT_DENY_MAC: i64 = 0 - 2 // forged / tampered / malformed (HMAC mismatch) -- fail-closed
14const CAPT_DENY_EXP: i64 = 0 - 3 // expired
15const CAPT_DENY_TOOL: i64 = 0 - 4 // valid capability, but it does NOT grant THIS tool (least-authority)
16const CAPT_DENY_REVOKED: i64 = 0 - 5 // valid + unexpired MAC, but the cap's nonce is on the revocation denylist
17
18func capt_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n }
19func capt_catb(d: *u8, o: i64, s: *u8, n: i64) -> i64 { var i: i64 = 0; while i < n { d[o + i] = s[i]; i = i + 1 } return o + n }
20func capt_catn(d: *u8, o: i64, v: i64) -> i64 {
21 let t: *u8 = sys_mmap(24); var m: i64 = v; var k: i64 = 0
22 if m == 0 { t[0] = 48 as u8; k = 1 }
23 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 }
24 var w: i64 = o; var i: i64 = 0
25 while i < k { d[w] = t[k - 1 - i]; w = w + 1; i = i + 1 }
26 return w
27}
28
29// build "<allow>~<exp>~<nonce>" into buf; returns length.
30func capt_payload(allow: *u8, allen: i64, exp: i64, nonce: i64, buf: *u8) -> i64 {
31 var o: i64 = capt_catb(buf, 0, allow, allen)
32 buf[o] = 0x7E as u8; o = o + 1 // '~'
33 o = capt_catn(buf, o, exp)
34 buf[o] = 0x7E as u8; o = o + 1
35 o = capt_catn(buf, o, nonce)
36 return o
37}
38
39// ISSUE a capability token (writes to out, returns length). The `key` is the issuing authority's HMAC secret.
40func capt_issue(key: *u8, klen: i64, allow: *u8, allen: i64, exp: i64, nonce: i64, out: *u8, cap: i64) -> i64 {
41 let pl: *u8 = sys_mmap(2048)
42 let pn: i64 = capt_payload(allow, allen, exp, nonce, pl)
43 return signed_cookie_sign(pl, pn, key, klen, out, cap)
44}
45
46// is tool[0..tlen) an EXACT member of the comma-separated allow[0..alen)? "*" grants all. Boundary-safe.
47func capt_allows(allow: *u8, alen: i64, tool: *u8, tlen: i64) -> i64 {
48 if alen == 1 { if allow[0] == (0x2A as u8) { return 1 } } // "*"
49 var s: i64 = 0; var i: i64 = 0
50 while i <= alen {
51 var sep: i64 = 0
52 if i == alen { sep = 1 } else { if allow[i] == (0x2C as u8) { sep = 1 } } // ','
53 if sep == 1 {
54 if i - s == tlen {
55 var m: i64 = 1; var c: i64 = 0
56 while c < tlen { if allow[s + c] != tool[c] { m = 0 } c = c + 1 }
57 if m == 1 { return 1 }
58 }
59 s = i + 1
60 }
61 i = i + 1
62 }
63 return 0
64}
65
66// VERIFY that a capability token grants `tool` at time `now`. CAPT_OK or a negative deny code. NO ambient identity is
67// consulted -- the token IS the authority (the ocap property that structurally forecloses confused-deputy). Fail-closed.
68// Recompute the HMAC over the payload and compare with the CORRECT sense. Returns 1 if the MAC is valid + sets
69// plen_out = payload length (offset 0..plen). ⚠ nx_ct's ct_memcmp returns 1-if-EQUAL (an equality predicate, NOT C
70// memcmp's 0-if-equal); nx_signed_cookie's signed_cookie_verify has this sense INVERTED (nx_signed_cookie.nx:116 ->
71// it accepts forgeries), so we do NOT use it -- we own the compare and require ct_memcmp == 1.
72func capt_mac_ok(key: *u8, klen: i64, token: *u8, tlen: i64, plen_out: *i64) -> i64 {
73 var dot: i64 = 0 - 1; var i: i64 = 0
74 while i < tlen { if token[i] == (0x2E as u8) { dot = i } i = i + 1 }
75 if dot < 0 { return 0 }
76 let slen: i64 = tlen - (dot + 1)
77 let exp: *u8 = sys_mmap(128)
78 let elen: i64 = sc_sign_bytes(key, klen, token, dot, exp) // b64url(HMAC-SHA256(key, payload)) -- the correct sign path
79 if elen != slen { return 0 }
80 if ct_memcmp(((token as i64) + dot + 1) as *u8, exp, slen) != 1 { return 0 }
81 plen_out[0] = dot
82 return 1
83}
84
85func capt_verify(key: *u8, klen: i64, token: *u8, tlen: i64, tool: *u8, toollen: i64, now: i64) -> i64 {
86 let plb: *i64 = sys_mmap(16) as *i64
87 if capt_mac_ok(key, klen, token, tlen, plb) == 0 { return CAPT_DENY_MAC } // forged/tampered/bad-key/malformed
88 let pl: *u8 = token
89 let pn: i64 = plb[0]
90 var d1: i64 = 0 - 1; var d2: i64 = 0 - 1; var i: i64 = 0
91 while i < pn { if pl[i] == (0x7E as u8) { if d1 < 0 { d1 = i } else { if d2 < 0 { d2 = i } } } i = i + 1 }
92 if d1 < 0 { return CAPT_DENY_MAC }
93 if d2 < 0 { return CAPT_DENY_MAC }
94 var exp: i64 = 0; var j: i64 = d1 + 1
95 while j < d2 { let c: i64 = pl[j] as i64; if c >= 48 { if c <= 57 { exp = exp * 10 + (c - 48) } } j = j + 1 }
96 if now >= exp { return CAPT_DENY_EXP }
97 if capt_allows(pl, d1, tool, toollen) == 1 { return CAPT_OK }
98 return CAPT_DENY_TOOL
99}
100
101// ATTENUATE: derive a NARROWER capability. Every tool in `narrow` MUST already be granted by the input token, so a
102// capability can only be WEAKENED, never widened (the ocap least-authority guarantee). Verifies the input first.
103// Returns the new token length, or -1 if the input is invalid OR `narrow` tries to widen.
104func capt_attenuate(key: *u8, klen: i64, token: *u8, tlen: i64, narrow: *u8, nlen: i64, exp: i64, nonce: i64, out: *u8, cap: i64) -> i64 {
105 let plb: *i64 = sys_mmap(16) as *i64
106 if capt_mac_ok(key, klen, token, tlen, plb) == 0 { return 0 - 1 }
107 let pl: *u8 = token
108 let pn: i64 = plb[0]
109 var d1: i64 = 0 - 1; var d2: i64 = 0 - 1; var i: i64 = 0
110 while i < pn { if pl[i] == (0x7E as u8) { if d1 < 0 { d1 = i } else { if d2 < 0 { d2 = i } } } i = i + 1 }
111 if d1 < 0 { return 0 - 1 }
112 if d2 < 0 { return 0 - 1 }
113 // clamp the delegated exp to the parent's -- attenuation must never EXTEND time-authority (exp' <= parent exp)
114 var pexp: i64 = 0; var je: i64 = d1 + 1
115 while je < d2 { let c: i64 = pl[je] as i64; if c >= 48 { if c <= 57 { pexp = pexp * 10 + (c - 48) } } je = je + 1 }
116 var cexp: i64 = exp
117 if pexp < cexp { cexp = pexp }
118 // every comma-item of `narrow` must be allowed by the original allow-set (subset-only)
119 var s: i64 = 0; var k: i64 = 0
120 while k <= nlen {
121 var sep: i64 = 0
122 if k == nlen { sep = 1 } else { if narrow[k] == (0x2C as u8) { sep = 1 } }
123 if sep == 1 {
124 if k > s { if capt_allows(pl, d1, ((narrow as i64) + s) as *u8, k - s) == 0 { return 0 - 1 } }
125 s = k + 1
126 }
127 k = k + 1
128 }
129 return capt_issue(key, klen, narrow, nlen, cexp, nonce, out, cap)
130}
131
132// extract the nonce (3rd payload field: after the 2nd '~', before the '.<sig>') from a token. -1 if malformed. Used by
133// the revocation denylist -- a cap is identified for revocation by its nonce, so nonces SHOULD be unique per cap.
134func capt_nonce_of(token: *u8, tlen: i64) -> i64 {
135 var dot: i64 = 0 - 1; var i: i64 = 0
136 while i < tlen { if token[i] == (0x2E as u8) { dot = i } i = i + 1 }
137 if dot < 0 { return 0 - 1 }
138 var d1: i64 = 0 - 1; var d2: i64 = 0 - 1; i = 0
139 while i < dot { if token[i] == (0x7E as u8) { if d1 < 0 { d1 = i } else { if d2 < 0 { d2 = i } } } i = i + 1 }
140 if d2 < 0 { return 0 - 1 }
141 var n: i64 = 0; var j: i64 = d2 + 1
142 while j < dot { let c: i64 = token[j] as i64; if c >= 48 { if c <= 57 { n = n * 10 + (c - 48) } } j = j + 1 }
143 return n
144}