nx_cap_token_candidate_t186.nx source
↩ module page · 181 lines · 10383 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_candidate_t187.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 var start:i64=o;if v<0{d[start]=45 as u8;start=start+1}
22 var count:i64=1;var m:i64=v;while m<=0-10 || m>=10{m=m/10;count=count+1}
23 var at:i64=start+count;m=v
24 while at>start{var digit:i64=m%10;if digit<0{digit=0-digit};at=at-1;d[at]=(48+digit) as u8;m=m/10}
25 return start+count
26}
27
28// build "<allow>~<exp>~<nonce>" into buf; returns length.
29func capt_payload(allow: *u8, allen: i64, exp: i64, nonce: i64, buf: *u8) -> i64 {
30 var o: i64 = capt_catb(buf, 0, allow, allen)
31 buf[o] = 0x7E as u8; o = o + 1 // '~'
32 o = capt_catn(buf, o, exp)
33 buf[o] = 0x7E as u8; o = o + 1
34 o = capt_catn(buf, o, nonce)
35 return o
36}
37
38// Wire extent derives from decimal fields and the SHA-256 signature encoding.
39const CAPT_SIGN_WORKSPACE:i64=SC_SIGNATURE_BYTES
40const CAPT_ERR_ALLOC:i64=0-12
41const CAPT_ERR_RELEASE:i64=0-13
42const CAPT_SIZE_MAX:i64=9223372036854775807
43func capt_decimal_bytes(value:i64)->i64{
44 if value<0{return 0-1};var n:i64=1;var v:i64=value
45 while v>=10{v=v/10;n=n+1};return n
46}
47func capt_issue_capacity(allen:i64,exp:i64,nonce:i64)->i64{
48 let ed:i64=capt_decimal_bytes(exp);let nd:i64=capt_decimal_bytes(nonce)
49 if allen<0 || ed<0 || nd<0{return SC_ERR_SHORT}
50 let extra:i64=ed+nd+2+1+SC_SIGNATURE_BYTES
51 if allen>CAPT_SIZE_MAX-extra{return SC_ERR_SHORT}
52 return allen+extra
53}
54func capt_issue_workspace_bytes(allen:i64,exp:i64,nonce:i64)->i64{
55 let token:i64=capt_issue_capacity(allen,exp,nonce);if token<0{return token}
56 let payload:i64=token-1-SC_SIGNATURE_BYTES
57 if payload>CAPT_SIZE_MAX-(SHA256_WORD_ALIGN-1){return SC_ERR_SHORT}
58 let aligned:i64=((payload+SHA256_WORD_ALIGN-1)/SHA256_WORD_ALIGN)*SHA256_WORD_ALIGN
59 let crypto:i64=sc_sign_workspace_bytes();if aligned>CAPT_SIZE_MAX-crypto{return SC_ERR_SHORT}
60 return aligned+crypto
61}
62func capt_issue_workspace(key:*u8,klen:i64,allow:*u8,allen:i64,exp:i64,nonce:i64,out:*u8,cap:i64,workspace:*u8,capacity:i64)->i64{
63 let needed:i64=capt_issue_capacity(allen,exp,nonce);let wn:i64=capt_issue_workspace_bytes(allen,exp,nonce)
64 if needed<0 || wn<0 || cap<needed{return SC_ERR_SHORT}
65 let base:i64=workspace as i64;let target:i64=out as i64
66 if sha256_checked_input(key,klen,out)!=1 || sha256_checked_input(allow,allen,out)!=1 || target>CAPT_SIZE_MAX-needed{return HMAC_E_INPUT}
67 if base<=0 || capacity<wn || base>CAPT_SIZE_MAX-wn || base%SHA256_WORD_ALIGN!=0{return HMAC_E_WORKSPACE}
68 if sha256_ranges_overlap(base,wn,key as i64,klen)==1 || sha256_ranges_overlap(base,wn,allow as i64,allen)==1 || sha256_ranges_overlap(base,wn,target,needed)==1{return HMAC_E_WORKSPACE}
69 let pn:i64=capt_payload(allow,allen,exp,nonce,workspace)
70 let aligned:i64=((pn+SHA256_WORD_ALIGN-1)/SHA256_WORD_ALIGN)*SHA256_WORD_ALIGN
71 return signed_cookie_sign_workspace(workspace,pn,key,klen,out,cap,workspace+aligned,wn-aligned)
72}
73func capt_issue(key:*u8,klen:i64,allow:*u8,allen:i64,exp:i64,nonce:i64,out:*u8,cap:i64)->i64{
74 let needed:i64=capt_issue_capacity(allen,exp,nonce);let wn:i64=capt_issue_workspace_bytes(allen,exp,nonce)
75 if needed<0 || wn<0 || cap<needed{return SC_ERR_SHORT}
76 let ws:*u8=sys_mmap_try(wn);if (ws as i64)<=0{return CAPT_ERR_ALLOC}
77 let rc:i64=capt_issue_workspace(key,klen,allow,allen,exp,nonce,out,cap,ws,wn)
78 let freed:i64=sys_munmap_direct(ws,wn);if freed!=0{return CAPT_ERR_RELEASE};return rc
79}
80
81// is tool[0..tlen) an EXACT member of the comma-separated allow[0..alen)? "*" grants all. Boundary-safe.
82func capt_allows(allow: *u8, alen: i64, tool: *u8, tlen: i64) -> i64 {
83 if alen == 1 { if allow[0] == (0x2A as u8) { return 1 } } // "*"
84 var s: i64 = 0; var i: i64 = 0
85 while i <= alen {
86 var sep: i64 = 0
87 if i == alen { sep = 1 } else { if allow[i] == (0x2C as u8) { sep = 1 } } // ','
88 if sep == 1 {
89 if i - s == tlen {
90 var m: i64 = 1; var c: i64 = 0
91 while c < tlen { if allow[s + c] != tool[c] { m = 0 } c = c + 1 }
92 if m == 1 { return 1 }
93 }
94 s = i + 1
95 }
96 i = i + 1
97 }
98 return 0
99}
100
101// VERIFY that a capability token grants `tool` at time `now`. CAPT_OK or a negative deny code. NO ambient identity is
102// consulted -- the token IS the authority (the ocap property that structurally forecloses confused-deputy). Fail-closed.
103// Recompute the HMAC over the payload and compare with the CORRECT sense. Returns 1 if the MAC is valid + sets
104// plen_out = payload length (offset 0..plen). ⚠ nx_ct's ct_memcmp returns 1-if-EQUAL (an equality predicate, NOT C
105// memcmp's 0-if-equal). The shared workspace verifier enforces that equality predicate.
106func capt_mac_ok(key:*u8,klen:i64,token:*u8,tlen:i64,plen_out:*i64)->i64{
107 let wn:i64=sc_verify_workspace_bytes();let ws:*u8=sys_mmap_try(wn)
108 if (ws as i64)<=0{return 0}
109 let rc:i64=signed_cookie_verify_workspace(token,tlen,key,klen,plen_out,ws,wn)
110 let freed:i64=sys_munmap_direct(ws,wn)
111 if rc!=0 || freed!=0{return 0};return 1
112}
113
114func capt_verify(key: *u8, klen: i64, token: *u8, tlen: i64, tool: *u8, toollen: i64, now: i64) -> i64 {
115 let plb:*i64=sys_mmap_try(__size_of(i64)) as *i64
116 if (plb as i64)<=0{return CAPT_ERR_ALLOC}
117 let valid:i64=capt_mac_ok(key,klen,token,tlen,plb)
118 let pn_saved:i64=plb[0];let freed:i64=sys_munmap_direct(plb as *u8,__size_of(i64))
119 if freed!=0{return CAPT_ERR_RELEASE}
120 if valid!=1 { return CAPT_DENY_MAC } // forged/tampered/bad-key/malformed
121 let pl: *u8 = token
122 let pn: i64 = pn_saved
123 var d1: i64 = 0 - 1; var d2: i64 = 0 - 1; var i: i64 = 0
124 while i < pn { if pl[i] == (0x7E as u8) { if d1 < 0 { d1 = i } else { if d2 < 0 { d2 = i } } } i = i + 1 }
125 if d1 < 0 { return CAPT_DENY_MAC }
126 if d2 < 0 { return CAPT_DENY_MAC }
127 var exp: i64 = 0; var j: i64 = d1 + 1
128 while j < d2 { let c: i64 = pl[j] as i64; if c >= 48 { if c <= 57 { exp = exp * 10 + (c - 48) } } j = j + 1 }
129 if now >= exp { return CAPT_DENY_EXP }
130 if capt_allows(pl, d1, tool, toollen) == 1 { return CAPT_OK }
131 return CAPT_DENY_TOOL
132}
133
134// ATTENUATE: derive a NARROWER capability. Every tool in `narrow` MUST already be granted by the input token, so a
135// capability can only be WEAKENED, never widened (the ocap least-authority guarantee). Verifies the input first.
136// Returns the new token length, or -1 if the input is invalid OR `narrow` tries to widen.
137func capt_attenuate(key: *u8, klen: i64, token: *u8, tlen: i64, narrow: *u8, nlen: i64, exp: i64, nonce: i64, out: *u8, cap: i64) -> i64 {
138 let plb:*i64=sys_mmap_try(__size_of(i64)) as *i64
139 if (plb as i64)<=0{return CAPT_ERR_ALLOC}
140 let valid:i64=capt_mac_ok(key,klen,token,tlen,plb)
141 let pn_saved:i64=plb[0];let freed:i64=sys_munmap_direct(plb as *u8,__size_of(i64))
142 if freed!=0{return CAPT_ERR_RELEASE}
143 if valid!=1{return 0-1}
144 let pl: *u8 = token
145 let pn: i64 = pn_saved
146 var d1: i64 = 0 - 1; var d2: i64 = 0 - 1; var i: i64 = 0
147 while i < pn { if pl[i] == (0x7E as u8) { if d1 < 0 { d1 = i } else { if d2 < 0 { d2 = i } } } i = i + 1 }
148 if d1 < 0 { return 0 - 1 }
149 if d2 < 0 { return 0 - 1 }
150 // clamp the delegated exp to the parent's -- attenuation must never EXTEND time-authority (exp' <= parent exp)
151 var pexp: i64 = 0; var je: i64 = d1 + 1
152 while je < d2 { let c: i64 = pl[je] as i64; if c >= 48 { if c <= 57 { pexp = pexp * 10 + (c - 48) } } je = je + 1 }
153 var cexp: i64 = exp
154 if pexp < cexp { cexp = pexp }
155 // every comma-item of `narrow` must be allowed by the original allow-set (subset-only)
156 var s: i64 = 0; var k: i64 = 0
157 while k <= nlen {
158 var sep: i64 = 0
159 if k == nlen { sep = 1 } else { if narrow[k] == (0x2C as u8) { sep = 1 } }
160 if sep == 1 {
161 if k > s { if capt_allows(pl, d1, ((narrow as i64) + s) as *u8, k - s) == 0 { return 0 - 1 } }
162 s = k + 1
163 }
164 k = k + 1
165 }
166 return capt_issue(key, klen, narrow, nlen, cexp, nonce, out, cap)
167}
168
169// extract the nonce (3rd payload field: after the 2nd '~', before the '.<sig>') from a token. -1 if malformed. Used by
170// the revocation denylist -- a cap is identified for revocation by its nonce, so nonces SHOULD be unique per cap.
171func capt_nonce_of(token: *u8, tlen: i64) -> i64 {
172 var dot: i64 = 0 - 1; var i: i64 = 0
173 while i < tlen { if token[i] == (0x2E as u8) { dot = i } i = i + 1 }
174 if dot < 0 { return 0 - 1 }
175 var d1: i64 = 0 - 1; var d2: i64 = 0 - 1; i = 0
176 while i < dot { if token[i] == (0x7E as u8) { if d1 < 0 { d1 = i } else { if d2 < 0 { d2 = i } } } i = i + 1 }
177 if d2 < 0 { return 0 - 1 }
178 var n: i64 = 0; var j: i64 = d2 + 1
179 while j < dot { let c: i64 = token[j] as i64; if c >= 48 { if c <= 57 { n = n * 10 + (c - 48) } } j = j + 1 }
180 return n
181}