code wiki / (root) / nx_cap_token.nx

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}