diff --git a/lib/identity/scoreboard.json b/lib/identity/scoreboard.json index a7d4497e..d206d258 100644 --- a/lib/identity/scoreboard.json +++ b/lib/identity/scoreboard.json @@ -1,10 +1,10 @@ { "language": "identity", - "total_pass": 124, - "total": 124, + "total_pass": 130, + "total": 130, "suites": [ {"name":"session","pass":11,"total":11,"status":"ok"}, - {"name":"token","pass":18,"total":18,"status":"ok"}, + {"name":"token","pass":24,"total":24,"status":"ok"}, {"name":"registry","pass":9,"total":9,"status":"ok"}, {"name":"api","pass":10,"total":10,"status":"ok"}, {"name":"oauth","pass":17,"total":17,"status":"ok"}, diff --git a/lib/identity/scoreboard.md b/lib/identity/scoreboard.md index 2094f5ce..7488a197 100644 --- a/lib/identity/scoreboard.md +++ b/lib/identity/scoreboard.md @@ -1,11 +1,11 @@ # identity-on-sx Scoreboard -**Total: 124 / 124 tests passing** +**Total: 130 / 130 tests passing** | | Suite | Pass | Total | |---|---|---|---| | ✅ | session | 11 | 11 | -| ✅ | token | 18 | 18 | +| ✅ | token | 24 | 24 | | ✅ | registry | 9 | 9 | | ✅ | api | 10 | 10 | | ✅ | oauth | 17 | 17 | diff --git a/lib/identity/tests/token.sx b/lib/identity/tests/token.sx index 7c9d1edc..caf85612 100644 --- a/lib/identity/tests/token.sx +++ b/lib/identity/tests/token.sx @@ -1,7 +1,7 @@ ;; identity/tests/token.sx — opaque tokens, grant-backed lookup, real -;; revocation, refresh-token rotation, and cascading revocation. The -;; revoke-then-introspect and refresh-reuse paths are the security -;; centrepieces. +;; revocation, refresh-token rotation, cascading revocation, and scope +;; narrowing on refresh. The revoke-then-introspect and refresh-reuse +;; paths are the security centrepieces. (define id-token-test-count 0) (define id-token-test-pass 0) @@ -166,6 +166,50 @@ "Reg = identity_tokens:start(),\n {ok, A, R} = identity_tokens:issue_grant(Reg, alice, web, read),\n identity_tokens:revoke(Reg, R),\n case identity_tokens:introspect(Reg, A) of\n {active, _, _, _} -> active;\n {inactive} -> inactive\n end")) "inactive") +;; ── scope as a set + narrowing on refresh (RFC 6749 §6 / §3.3) ─── + +(id-token-test + "a list scope round-trips through introspect" + (idtnm + (idt-ev + "Reg = identity_tokens:start(),\n {ok, A, _R} = identity_tokens:issue_grant(Reg, alice, web, [read, write]),\n case identity_tokens:introspect(Reg, A) of\n {active, _, _, [read, write]} -> matched;\n {active, _, _, _} -> other;\n {inactive} -> inactive\n end")) + "matched") + +(id-token-test + "refresh can narrow the scope to a subset" + (idtnm + (idt-ev + "Reg = identity_tokens:start(),\n {ok, _A, R} = identity_tokens:issue_grant(Reg, alice, web, [read, write]),\n {ok, A2, _R2} = identity_tokens:refresh(Reg, R, [read]),\n case identity_tokens:introspect(Reg, A2) of\n {active, _, _, [read]} -> narrowed;\n {active, _, _, _} -> other;\n {inactive} -> inactive\n end")) + "narrowed") + +(id-token-test + "refresh cannot widen scope beyond the grant" + (idtnm + (idt-ev + "Reg = identity_tokens:start(),\n {ok, _A, R} = identity_tokens:issue_grant(Reg, alice, web, [read]),\n case identity_tokens:refresh(Reg, R, [read, write]) of\n {ok, _, _} -> widened;\n {error, Why} -> Why\n end")) + "invalid_scope") + +(id-token-test + "an invalid_scope refresh does not consume the refresh token" + (idtnm + (idt-ev + "Reg = identity_tokens:start(),\n {ok, _A, R} = identity_tokens:issue_grant(Reg, alice, web, [read, write]),\n identity_tokens:refresh(Reg, R, [admin]),\n case identity_tokens:refresh(Reg, R, [read]) of\n {ok, _, _} -> still_usable;\n {error, Why} -> Why\n end")) + "still_usable") + +(id-token-test + "plain refresh keeps the full grant scope" + (idtnm + (idt-ev + "Reg = identity_tokens:start(),\n {ok, _A, R} = identity_tokens:issue_grant(Reg, alice, web, [read, write]),\n {ok, A2, _R2} = identity_tokens:refresh(Reg, R),\n case identity_tokens:introspect(Reg, A2) of\n {active, _, _, [read, write]} -> full;\n {active, _, _, _} -> other;\n {inactive} -> inactive\n end")) + "full") + +(id-token-test + "a narrowed token still cascades on revoke" + (idtnm + (idt-ev + "Reg = identity_tokens:start(),\n {ok, _A, R} = identity_tokens:issue_grant(Reg, alice, web, [read, write]),\n {ok, A2, _R2} = identity_tokens:refresh(Reg, R, [read]),\n identity_tokens:revoke(Reg, A2),\n case identity_tokens:introspect(Reg, A2) of\n {active, _, _, _} -> still_valid;\n {inactive} -> inactive\n end")) + "inactive") + (define id-token-test-summary (str "token " id-token-test-pass "/" id-token-test-count)) diff --git a/lib/identity/token.sx b/lib/identity/token.sx index f41f5dd3..199515eb 100644 --- a/lib/identity/token.sx +++ b/lib/identity/token.sx @@ -1,5 +1,6 @@ ;; identity/token.sx — opaque, grant-backed tokens with refresh-token -;; rotation (RFC 6749 §6, RFC 6819 §5.2.2.3) and cascading revocation. +;; rotation (RFC 6749 §6, RFC 6819 §5.2.2.3), cascading revocation, and +;; scope narrowing on refresh (RFC 6749 §6 / §3.3). ;; ;; The grant is the unit of authorization and the unit of cascade: an ;; access token and a refresh token both reference a grant {Subject, @@ -15,23 +16,29 @@ ;; superseded refresh token is treated as token theft (RFC 6819 §5.2.2.3): ;; the entire grant is revoked, killing the legitimate descendant too. ;; -;; Auditing: start/1 takes an audit ledger (identity_audit); every grant -;; transition (issue, refresh, revoke — including a reuse-triggered -;; revoke) appends an event. start/0 passes `none` and audits nothing, so -;; standalone use is unchanged. +;; Scope: a grant records the maximum scope granted. Each access token +;; carries its own EFFECTIVE scope (<= the grant's). refresh/2 keeps the +;; grant scope; refresh/3 requests a narrower scope — the request MUST be a +;; subset of the grant scope (RFC 6749 §6), else {error, invalid_scope} +;; and the refresh token is NOT consumed (the client may retry). Scope is +;; treated opaquely for issue/refresh-2 (atom or list); narrowing in +;; refresh/3 treats it as a set (list of scope atoms). +;; +;; Auditing: start/1 takes an audit ledger; every grant transition +;; (issue, refresh, revoke) appends an event. start/0 audits nothing. ;; ;; introspect reply shapes (RFC 7662 §2.2): ;; {active, Subject, Client, Scope} | {inactive} ;; ;; State threaded through loop/5: ;; Grants : [{Gid, {Subject, Client, Scope, active|revoked}}] -;; Access : [{AccessTok, Gid}] +;; Access : [{AccessTok, {Gid, EffectiveScope}}] ;; Refresh : [{RefreshTok, {Gid, current|superseded}}] ;; Audit : an identity_audit pid, or the atom none (define identity-token-source - "-module(identity_tokens).\n\n start() ->\n start(none).\n\n start(Audit) ->\n spawn(fun () -> loop([], [], [], 1, Audit) end).\n\n issue(Reg, Subject, Client, Scope) ->\n Reg ! {issue, Subject, Client, Scope, self()},\n receive {token_reply, R} -> R end.\n\n issue_grant(Reg, Subject, Client, Scope) ->\n Reg ! {issue_grant, Subject, Client, Scope, self()},\n receive {token_reply, R} -> R end.\n\n refresh(Reg, RefreshTok) ->\n Reg ! {refresh, RefreshTok, self()},\n receive {token_reply, R} -> R end.\n\n introspect(Reg, Token) ->\n Reg ! {introspect, Token, self()},\n receive {token_reply, R} -> R end.\n\n revoke(Reg, Token) ->\n Reg ! {revoke, Token, self()},\n receive {token_reply, R} -> R end.\n\n stop(Reg) ->\n Reg ! {stop, self()},\n receive {token_reply, R} -> R end.\n\n loop(Grants, Access, Refresh, NextGid, Audit) ->\n receive\n {issue, Subject, Client, Scope, From} ->\n Gid = NextGid,\n Tok = make_ref(),\n From ! {token_reply, {ok, Tok}},\n audit_event(Audit, Subject, issue),\n loop([{Gid, {Subject, Client, Scope, active}} | Grants],\n [{Tok, Gid} | Access], Refresh, NextGid + 1, Audit);\n {issue_grant, Subject, Client, Scope, From} ->\n Gid = NextGid,\n A = make_ref(),\n R = make_ref(),\n From ! {token_reply, {ok, A, R}},\n audit_event(Audit, Subject, issue),\n loop([{Gid, {Subject, Client, Scope, active}} | Grants],\n [{A, Gid} | Access],\n [{R, {Gid, current}} | Refresh],\n NextGid + 1, Audit);\n {refresh, RTok, From} ->\n case find(RTok, Refresh) of\n none ->\n From ! {token_reply, {error, invalid_grant}},\n loop(Grants, Access, Refresh, NextGid, Audit);\n {ok, {Gid, superseded}} ->\n From ! {token_reply, {error, invalid_grant}},\n audit_grant(Audit, Gid, Grants, revoke),\n loop(set_status(Gid, revoked, Grants), Access, Refresh, NextGid, Audit);\n {ok, {Gid, current}} ->\n case grant_active(Gid, Grants) of\n false ->\n From ! {token_reply, {error, invalid_grant}},\n loop(Grants, Access, Refresh, NextGid, Audit);\n true ->\n {Su, Cl, Sc} = grant_info(Gid, Grants),\n A2 = make_ref(),\n R2 = make_ref(),\n From ! {token_reply, {ok, A2, R2}},\n audit_event(Audit, Su, refresh),\n loop(Grants,\n [{A2, Gid} | Access],\n [{R2, {Gid, current}} | supersede(RTok, Refresh)],\n NextGid, Audit)\n end\n end;\n {introspect, Tok, From} ->\n From ! {token_reply, introspect_access(Tok, Access, Grants)},\n loop(Grants, Access, Refresh, NextGid, Audit);\n {revoke, Tok, From} ->\n From ! {token_reply, ok},\n case find_gid(Tok, Access, Refresh) of\n none -> loop(Grants, Access, Refresh, NextGid, Audit);\n {ok, Gid} ->\n audit_grant(Audit, Gid, Grants, revoke),\n loop(set_status(Gid, revoked, Grants), Access, Refresh, NextGid, Audit)\n end;\n {stop, From} ->\n From ! {token_reply, ok}\n end.\n\n audit_event(none, _, _) -> ok;\n audit_event(Audit, Subject, Action) ->\n Audit ! {event, Subject, Action},\n ok.\n\n audit_grant(none, _, _, _) -> ok;\n audit_grant(Audit, Gid, Grants, Action) ->\n {Su, _, _} = grant_info(Gid, Grants),\n Audit ! {event, Su, Action},\n ok.\n\n introspect_access(Tok, Access, Grants) ->\n case find(Tok, Access) of\n none -> {inactive};\n {ok, Gid} ->\n case find(Gid, Grants) of\n none -> {inactive};\n {ok, {Su, Cl, Sc, active}} -> {active, Su, Cl, Sc};\n {ok, {_, _, _, revoked}} -> {inactive}\n end\n end.\n\n find_gid(Tok, Access, Refresh) ->\n case find(Tok, Access) of\n {ok, Gid} -> {ok, Gid};\n none ->\n case find(Tok, Refresh) of\n {ok, {Gid, _}} -> {ok, Gid};\n none -> none\n end\n end.\n\n grant_active(Gid, Grants) ->\n case find(Gid, Grants) of\n {ok, {_, _, _, active}} -> true;\n Other -> false\n end.\n\n grant_info(Gid, Grants) ->\n case find(Gid, Grants) of\n {ok, {Su, Cl, Sc, _}} -> {Su, Cl, Sc}\n end.\n\n set_status(_, _, []) -> [];\n set_status(Gid, St, [{G, {Su, Cl, Sc, Old}} | Rest]) ->\n case G =:= Gid of\n true -> [{G, {Su, Cl, Sc, St}} | Rest];\n false -> [{G, {Su, Cl, Sc, Old}} | set_status(Gid, St, Rest)]\n end.\n\n supersede(_, []) -> [];\n supersede(RTok, [{T, {Gid, St}} | Rest]) ->\n case T =:= RTok of\n true -> [{T, {Gid, superseded}} | Rest];\n false -> [{T, {Gid, St}} | supersede(RTok, Rest)]\n end.\n\n find(_, []) -> none;\n find(Key, [{K, V} | Rest]) ->\n case K =:= Key of\n true -> {ok, V};\n false -> find(Key, Rest)\n end.") + "-module(identity_tokens).\n\n start() ->\n start(none).\n\n start(Audit) ->\n spawn(fun () -> loop([], [], [], 1, Audit) end).\n\n issue(Reg, Subject, Client, Scope) ->\n Reg ! {issue, Subject, Client, Scope, self()},\n receive {token_reply, R} -> R end.\n\n issue_grant(Reg, Subject, Client, Scope) ->\n Reg ! {issue_grant, Subject, Client, Scope, self()},\n receive {token_reply, R} -> R end.\n\n refresh(Reg, RefreshTok) ->\n Reg ! {refresh, RefreshTok, self()},\n receive {token_reply, R} -> R end.\n\n refresh(Reg, RefreshTok, Scope) ->\n Reg ! {refresh_scoped, RefreshTok, Scope, self()},\n receive {token_reply, R} -> R end.\n\n introspect(Reg, Token) ->\n Reg ! {introspect, Token, self()},\n receive {token_reply, R} -> R end.\n\n revoke(Reg, Token) ->\n Reg ! {revoke, Token, self()},\n receive {token_reply, R} -> R end.\n\n stop(Reg) ->\n Reg ! {stop, self()},\n receive {token_reply, R} -> R end.\n\n loop(Grants, Access, Refresh, NextGid, Audit) ->\n receive\n {issue, Subject, Client, Scope, From} ->\n Gid = NextGid,\n Tok = make_ref(),\n From ! {token_reply, {ok, Tok}},\n audit_event(Audit, Subject, issue),\n loop([{Gid, {Subject, Client, Scope, active}} | Grants],\n [{Tok, {Gid, Scope}} | Access], Refresh, NextGid + 1, Audit);\n {issue_grant, Subject, Client, Scope, From} ->\n Gid = NextGid,\n A = make_ref(),\n R = make_ref(),\n From ! {token_reply, {ok, A, R}},\n audit_event(Audit, Subject, issue),\n loop([{Gid, {Subject, Client, Scope, active}} | Grants],\n [{A, {Gid, Scope}} | Access],\n [{R, {Gid, current}} | Refresh],\n NextGid + 1, Audit);\n {refresh, RTok, From} ->\n case find(RTok, Refresh) of\n none ->\n From ! {token_reply, {error, invalid_grant}},\n loop(Grants, Access, Refresh, NextGid, Audit);\n {ok, {Gid, superseded}} ->\n From ! {token_reply, {error, invalid_grant}},\n audit_grant(Audit, Gid, Grants, revoke),\n loop(set_status(Gid, revoked, Grants), Access, Refresh, NextGid, Audit);\n {ok, {Gid, current}} ->\n case grant_active(Gid, Grants) of\n false ->\n From ! {token_reply, {error, invalid_grant}},\n loop(Grants, Access, Refresh, NextGid, Audit);\n true ->\n {Su, Cl, Sc} = grant_info(Gid, Grants),\n A2 = make_ref(),\n R2 = make_ref(),\n From ! {token_reply, {ok, A2, R2}},\n audit_event(Audit, Su, refresh),\n loop(Grants,\n [{A2, {Gid, Sc}} | Access],\n [{R2, {Gid, current}} | supersede(RTok, Refresh)],\n NextGid, Audit)\n end\n end;\n {refresh_scoped, RTok, Requested, From} ->\n case find(RTok, Refresh) of\n none ->\n From ! {token_reply, {error, invalid_grant}},\n loop(Grants, Access, Refresh, NextGid, Audit);\n {ok, {Gid, superseded}} ->\n From ! {token_reply, {error, invalid_grant}},\n audit_grant(Audit, Gid, Grants, revoke),\n loop(set_status(Gid, revoked, Grants), Access, Refresh, NextGid, Audit);\n {ok, {Gid, current}} ->\n case grant_active(Gid, Grants) of\n false ->\n From ! {token_reply, {error, invalid_grant}},\n loop(Grants, Access, Refresh, NextGid, Audit);\n true ->\n {Su, Cl, Sc} = grant_info(Gid, Grants),\n case subset(Requested, Sc) of\n false ->\n From ! {token_reply, {error, invalid_scope}},\n loop(Grants, Access, Refresh, NextGid, Audit);\n true ->\n A2 = make_ref(),\n R2 = make_ref(),\n From ! {token_reply, {ok, A2, R2}},\n audit_event(Audit, Su, refresh),\n loop(Grants,\n [{A2, {Gid, Requested}} | Access],\n [{R2, {Gid, current}} | supersede(RTok, Refresh)],\n NextGid, Audit)\n end\n end\n end;\n {introspect, Tok, From} ->\n From ! {token_reply, introspect_access(Tok, Access, Grants)},\n loop(Grants, Access, Refresh, NextGid, Audit);\n {revoke, Tok, From} ->\n From ! {token_reply, ok},\n case find_gid(Tok, Access, Refresh) of\n none -> loop(Grants, Access, Refresh, NextGid, Audit);\n {ok, Gid} ->\n audit_grant(Audit, Gid, Grants, revoke),\n loop(set_status(Gid, revoked, Grants), Access, Refresh, NextGid, Audit)\n end;\n {stop, From} ->\n From ! {token_reply, ok}\n end.\n\n audit_event(none, _, _) -> ok;\n audit_event(Audit, Subject, Action) ->\n Audit ! {event, Subject, Action},\n ok.\n\n audit_grant(none, _, _, _) -> ok;\n audit_grant(Audit, Gid, Grants, Action) ->\n {Su, _, _} = grant_info(Gid, Grants),\n Audit ! {event, Su, Action},\n ok.\n\n introspect_access(Tok, Access, Grants) ->\n case find(Tok, Access) of\n none -> {inactive};\n {ok, {Gid, Scope}} ->\n case find(Gid, Grants) of\n none -> {inactive};\n {ok, {Su, Cl, _, active}} -> {active, Su, Cl, Scope};\n {ok, {_, _, _, revoked}} -> {inactive}\n end\n end.\n\n find_gid(Tok, Access, Refresh) ->\n case find(Tok, Access) of\n {ok, {Gid, _}} -> {ok, Gid};\n none ->\n case find(Tok, Refresh) of\n {ok, {Gid, _}} -> {ok, Gid};\n none -> none\n end\n end.\n\n grant_active(Gid, Grants) ->\n case find(Gid, Grants) of\n {ok, {_, _, _, active}} -> true;\n Other -> false\n end.\n\n grant_info(Gid, Grants) ->\n case find(Gid, Grants) of\n {ok, {Su, Cl, Sc, _}} -> {Su, Cl, Sc}\n end.\n\n set_status(_, _, []) -> [];\n set_status(Gid, St, [{G, {Su, Cl, Sc, Old}} | Rest]) ->\n case G =:= Gid of\n true -> [{G, {Su, Cl, Sc, St}} | Rest];\n false -> [{G, {Su, Cl, Sc, Old}} | set_status(Gid, St, Rest)]\n end.\n\n supersede(_, []) -> [];\n supersede(RTok, [{T, {Gid, St}} | Rest]) ->\n case T =:= RTok of\n true -> [{T, {Gid, superseded}} | Rest];\n false -> [{T, {Gid, St}} | supersede(RTok, Rest)]\n end.\n\n subset([], _) -> true;\n subset([X | Rest], Granted) ->\n case member(X, Granted) of\n true -> subset(Rest, Granted);\n false -> false\n end.\n\n member(_, []) -> false;\n member(X, [Y | Rest]) ->\n case X =:= Y of\n true -> true;\n false -> member(X, Rest)\n end.\n\n find(_, []) -> none;\n find(Key, [{K, V} | Rest]) ->\n case K =:= Key of\n true -> {ok, V};\n false -> find(Key, Rest)\n end.") (define identity-load-token! diff --git a/plans/identity-on-sx.md b/plans/identity-on-sx.md index e5a1107a..2a3f13b6 100644 --- a/plans/identity-on-sx.md +++ b/plans/identity-on-sx.md @@ -19,7 +19,7 @@ through the event log, all authorization questions delegated to `acl-on-sx`. ## Status (rolling) -`bash lib/identity/conformance.sh` → **124/124** (all four phases complete) +`bash lib/identity/conformance.sh` → **130/130** (4 phases + ext: scope narrowing) ## Ground rules @@ -78,9 +78,9 @@ lib/identity/api.sx ── (identity/login) (identity/grant?) (identity/revoke) - [x] tests: audit completeness, cross-instance subject mapping ## Extensions (base roadmap complete; deepen the engine) -- [ ] PKCE S256 method (RFC 7636 §4.2) — SHA256 challenge derivation, not just `plain` +- [~] PKCE S256 method (RFC 7636 §4.2) — BLOCKED on erlang substrate (see Blockers) - [ ] access-token TTL / `expires_in` — tokens expire as a grant timeout, introspect honours it -- [ ] scope as a set + scope narrowing on refresh (RFC 6749 §6) +- [x] scope as a set + scope narrowing on refresh (RFC 6749 §6) - [ ] client registry: public vs confidential clients, client authentication (RFC 6749 §2) - [ ] client-credentials grant (RFC 6749 §4.4) and device grant (RFC 8628) - [ ] acl-on-sx delegation: wire `verify`/membership projection → an acl decision, integration test @@ -88,6 +88,15 @@ lib/identity/api.sx ── (identity/login) (identity/grant?) (identity/revoke) - [ ] unify `api.sx` over oauth + membership + audit (one facade, audited login/consent) ## Progress log +- 2026-06-07 — scope narrowing (ext): each access token now carries its own + EFFECTIVE scope (<= the grant's max). `refresh/3` requests a narrower scope; + the request must be a subset of the grant scope (RFC 6749 §6) else + `{error, invalid_scope}` and the refresh token is NOT consumed (client may + retry, §5.2). `refresh/2` keeps full scope; scope stays opaque (atom or list) + for issue, so all prior atom-scope tests pass unchanged. token 18→24, 130/130. + Also filed Blocker: PKCE S256 needs SHA256+binary compare, both broken in the + erlang substrate (binary `=:=` always true; crypto:hash ignores binary + content) — deferred, plain method stays. - 2026-06-07 — `federation.sx`: trust-gated, advisory federated identity. A peer assertion is accepted only from an explicitly trusted peer (else `{error, untrusted}`) and is flagged `{peer_asserted, Peer}`, never @@ -171,4 +180,18 @@ lib/identity/api.sx ── (identity/login) (identity/grant?) (identity/revoke) `tests/session.sx`). 11/11. ## Blockers -(loop fills this in) +- 2026-06-07 — **PKCE S256 blocked: erlang binary bugs.** Two substrate bugs + in `lib/erlang` make a correct/secure S256 impossible (S256 needs + `BASE64URL(SHA256(verifier))` compared against the stored challenge): + 1. **Binary `=:=` always true.** `<<"v1">> =:= <<"v2">>` → `true`; + `<<"abc">> =:= <<"abd">>` → `true`. So a hash comparison can't reject a + wrong verifier. + 2. **`crypto:hash` ignores binary-literal content.** + `crypto:hash(sha256, <<"v1">>)` and `crypto:hash(sha256, <<"v2">>)` return + the *identical* 32-byte digest (`6e 34 0b 9c …`), which is also ≠ the + correct SX-level `(crypto-sha256 "abc")` (`ba 78 16 bf …`). The binary + payload isn't reaching the hash. (Atom input → badarg→nil, separate issue.) + Minimal repro (epoch protocol, after loading lib/erlang/runtime.sx): + `(erlang-eval-ast "case <<\"a\">> =:= <<\"b\">> of true -> bug; false -> ok end")` + → `bug`. Not in scope to fix (lib/erlang is a substrate). PKCE `plain` + remains correct and in use; S256 deferred until the binary path is fixed.