code wiki / (root) / nx_cap_token_candidate_t186.nx

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}