Skip to content

Commit df0ee1a

Browse files
committed
feat(policy): model l4 envelope and narrowness checks
1 parent 46cc861 commit df0ee1a

3 files changed

Lines changed: 930 additions & 103 deletions

File tree

crates/openshell-prover/MAXIMUM_POLICY_ENVELOPE_SPIKE.md

Lines changed: 149 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,25 @@ exists x:
1717

1818
Rust normalizes schema-level OpenShell policy semantics, such as access presets
1919
and unsupported field detection. Z3 owns the action variables (`binary`, `host`,
20-
`port`, `protocol`, `method`, and `path`) and checks whether any symbolic action
21-
is allowed by the candidate but not by the maximum. Host, path, and binary globs
20+
`port`, `layer`, `method`, and `path`) and checks whether any symbolic action is
21+
allowed by the candidate but not by the maximum. Host, path, and binary globs
2222
compile to Z3 regular-expression constraints. If the solver finds such an `x`,
2323
the candidate exceeds the maximum. If no such `x` exists, the candidate is within
2424
the modeled maximum. If either policy uses a surface the spike does not model
2525
yet, the check fails closed as `Unsupported`.
2626

27+
The first modeled action layers are:
28+
29+
```text
30+
L4: binary, host, port
31+
REST: binary, host, port, method, path
32+
WebSocket: binary, host, port, method, path
33+
```
34+
35+
An L4 endpoint is broader than inspected protocols: it covers raw L4, REST, and
36+
WebSocket actions on the same binary/host/port. REST and WebSocket endpoints
37+
cover only their own inspected action layer.
38+
2739
## Demo 1: Narrow Candidate Within Maximum
2840

2941
Maximum:
@@ -155,11 +167,66 @@ port: 443 -> [443, 8443]
155167

156168
Why: each change creates at least one action that the maximum does not allow.
157169

158-
L4-only endpoints are not modeled in this spike. They fail closed as
159-
`Unsupported` rather than being compared against the REST-only symbolic action
160-
domain.
170+
## Demo 5: L4, REST, And WebSocket Layering
171+
172+
An L4 maximum covers a narrower REST candidate on the same binary/host/port:
173+
174+
```yaml
175+
maximum:
176+
endpoints:
177+
- host: api.github.com
178+
port: 443
179+
180+
candidate:
181+
endpoints:
182+
- host: api.github.com
183+
port: 443
184+
protocol: rest
185+
enforcement: enforce
186+
access: read-write
187+
```
188+
189+
Result:
190+
191+
```text
192+
WithinMax
193+
```
194+
195+
A REST maximum does not cover a raw L4 candidate:
161196

162-
## Demo 5: Unsupported Surfaces Fail Closed
197+
```text
198+
ExceedsMax {
199+
protocol: "l4",
200+
host: "api.github.com",
201+
port: 443,
202+
...
203+
}
204+
```
205+
206+
Why: raw L4 egress is broader and less inspected than an enforced REST or
207+
WebSocket surface.
208+
209+
WebSocket endpoints are modeled as their own inspected layer with `GET` and
210+
`WEBSOCKET_TEXT` actions. `read-only` WebSocket access covers the opening
211+
`GET`, not text send authority. Credential rewrite flags fail closed until the
212+
prover has an explicit authority check for introducing credential injection.
213+
214+
## Demo 6: Credentialed L4 Is A Separate High-Risk Check
215+
216+
The spike also includes a separate check for uninspected credentialed L4 reach:
217+
218+
```text
219+
exists x:
220+
policy_allows(x)
221+
AND x.layer = L4
222+
AND credential_target_matches(x.host)
223+
```
224+
225+
This is intentionally separate from maximum containment. It lets product policy
226+
decide whether credentialed L4 should be explicitly allowed, auto-denied, or
227+
sent to human review without changing the meaning of the signed maximum.
228+
229+
## Demo 7: Unsupported Surfaces Fail Closed
163230

164231
Maximum:
165232

@@ -189,7 +256,7 @@ GraphQL operation and field constraints
189256
MCP tool/resource constraints
190257
CIDR-only allowed_ips
191258
endpoint path scoping
192-
L4-only endpoints
259+
credential rewrite flags
193260
```
194261

195262
## Narrowness Companion
@@ -227,9 +294,11 @@ exact new grant: +1
227294
single path wildcard: +2
228295
host/binary wildcard: +3
229296
recursive glob (**): +6
297+
L7-to-L4 broadening: +8
230298
```
231299

232-
A conservative budget can allow one exact grant and reject recursive globs:
300+
A conservative budget can allow one exact grant, including an exact L4
301+
host/port grant, while rejecting recursive globs and L7-to-L4 broadening:
233302

234303
```rust
235304
NarrownessBudget {
@@ -239,7 +308,12 @@ NarrownessBudget {
239308
}
240309
```
241310

242-
## Demo 6: One Exact Path Fits A Narrow Budget
311+
Credentialed raw L4 is handled by the separate credentialed-L4 check above. A
312+
production auto-approval gate can combine that result with this budget, for
313+
example by assigning a high score or requiring human review when raw L4 reaches
314+
a credential target.
315+
316+
## Demo 8: One Exact Path Fits A Narrow Budget
243317

244318
Current:
245319

@@ -280,7 +354,59 @@ WithinBudget {
280354
Why: the candidate adds one exact modeled grant. The grant allows both `GET` and
281355
runtime-implied `HEAD`, but the budget counts the source grant once.
282356

283-
## Demo 7: Lazy Recursive Path Exceeds A Narrow Budget
357+
## Demo 9: Exact L4 Versus L7-To-L4 Broadening
358+
359+
An exact L4 grant to a new normal host is still one exact grant:
360+
361+
```yaml
362+
candidate:
363+
endpoints:
364+
- host: files.pythonhosted.org
365+
port: 443
366+
```
367+
368+
Result:
369+
370+
```text
371+
WithinBudget {
372+
total_score: 1,
373+
reasons: [NewGrant]
374+
}
375+
```
376+
377+
If the current policy already has inspected REST/WebSocket access for the same
378+
authority and the candidate asks for raw L4, the same exact host/port becomes a
379+
larger jump:
380+
381+
```yaml
382+
current:
383+
endpoints:
384+
- host: api.github.com
385+
port: 443
386+
protocol: rest
387+
enforcement: enforce
388+
access: full
389+
390+
candidate:
391+
endpoints:
392+
- host: api.github.com
393+
port: 443
394+
```
395+
396+
Result:
397+
398+
```text
399+
ExceedsBudget {
400+
total_score: 9,
401+
reasons: [NewGrant, L7ToL4Broadening]
402+
}
403+
```
404+
405+
Why: the candidate is not just adding a host. It is replacing inspected
406+
method/path reasoning with raw host/port authority for a service the current
407+
policy already modeled at L7.
408+
409+
## Demo 10: Lazy Recursive Path Exceeds A Narrow Budget
284410

285411
Current:
286412

@@ -330,29 +456,22 @@ it.
330456
mise exec -- cargo test -p openshell-prover
331457
```
332458

333-
Maximal-policy tests include:
459+
Coverage includes:
334460

335461
```text
336-
envelope::tests::exact_rest_rule_is_within_maximum
337-
envelope::tests::broader_rest_path_exceeds_maximum
338-
envelope::tests::method_escalation_exceeds_maximum
339-
envelope::tests::host_wildcard_broadening_exceeds_maximum
340-
envelope::tests::binary_wildcard_broadening_exceeds_maximum
341-
envelope::tests::port_broadening_exceeds_maximum
342-
envelope::tests::l4_candidate_exceeds_l7_maximum
343-
envelope::tests::maximum_wildcards_cover_exact_candidate
344-
envelope::tests::deny_rules_are_unsupported_until_modeled
345-
envelope::tests::query_graphql_cidr_and_mcp_surfaces_are_unsupported
346-
envelope::tests::narrowness_allows_one_exact_path_delta_within_budget
347-
envelope::tests::narrowness_allows_one_exact_method_delta_within_budget
348-
envelope::tests::narrowness_rejects_multiple_exact_grants_over_budget
349-
envelope::tests::narrowness_rejects_recursive_path_glob_as_too_broad
462+
REST method/path/host/binary/port containment
463+
L4 versus REST/WebSocket layer containment
464+
WebSocket text-send and read-only behavior
465+
credentialed raw L4 detection
466+
fail-closed unsupported deny/query/GraphQL/MCP/CIDR/rewrite surfaces
467+
exact, wildcard, recursive-glob, L4, and WebSocket narrowness deltas
468+
representative provider-shaped accept/reject cases
350469
```
351470

352471
## Readout
353472

354-
This validates the product shape for REST/path/binary/host/port maximum-policy
355-
envelopes and narrowness budgets with symbolic Z3 counterexample queries. Deny
356-
rules, MCP, GraphQL, query constraints, CIDR, and L4-only endpoints can land as
357-
follow-on modeled surfaces once the containment and delta mechanics are proven
358-
useful.
473+
This validates the product shape for L4, REST, and WebSocket maximum-policy
474+
envelopes, narrowness budgets, and uninspected credentialed L4 detection with
475+
symbolic Z3 counterexample queries. Deny rules, MCP, GraphQL, query constraints,
476+
and CIDR can land as follow-on modeled surfaces once the containment and delta
477+
mechanics are proven useful.

crates/openshell-prover/README.md

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,22 +46,25 @@ exists x:
4646
AND NOT maximum_allows(x)
4747
```
4848

49-
The first implementation models REST allow surfaces for binaries,
50-
hosts, ports, protocols, methods, paths, access presets, and the
49+
The first implementation models L4, REST, and WebSocket allow surfaces
50+
for binaries, hosts, ports, methods, paths, access presets, and the
5151
supported host/path/binary glob subset. Rust normalizes policy schema
5252
details such as access presets and fail-closed unsupported fields, then
5353
encodes candidate and maximum allow predicates as Z3 constraints over
54-
symbolic action variables (`binary`, `host`, `port`, `protocol`,
55-
`method`, and `path`). Host, path, and binary globs are encoded as Z3
56-
regular-expression membership constraints. Query parameters are not
54+
symbolic action variables (`binary`, `host`, `port`, `layer`, `method`,
55+
and `path`). L4 endpoints cover raw L4, REST, and WebSocket actions on
56+
the same binary/host/port; REST and WebSocket endpoints cover only their
57+
own inspected action layer. Host, path, and binary globs are encoded as
58+
Z3 regular-expression membership constraints. Query parameters are not
5759
modeled yet. It returns:
5860

5961
- `WithinMax` when every modeled candidate action is covered.
6062
- `ExceedsMax` with a solver-produced counterexample when the candidate
6163
is broader.
6264
- `Unsupported` when either policy uses a surface not modeled yet, such
6365
as deny rules, query constraints, GraphQL controls, MCP controls,
64-
endpoint path scoping, CIDR-only `allowed_ips`, or L4-only endpoints.
66+
endpoint path scoping, credential rewrite flags, or CIDR-only
67+
`allowed_ips`.
6568

6669
Unsupported is intentionally fail-closed. Future work can flip those
6770
fixtures to modeled results as deny rules, MCP, GraphQL, and richer
@@ -71,11 +74,16 @@ The spike also includes a narrowness-budget check for candidate updates
7174
against the current policy. It uses the same symbolic action model to
7275
prove whether candidate grants add reach, then scores the shape of the
7376
delta so exact grants can fit a small budget while broad globs such as
74-
`**` exceed it. See
77+
`**` and L7-to-L4 broadening exceed the conservative default. See
7578
[`MAXIMUM_POLICY_ENVELOPE_SPIKE.md`](MAXIMUM_POLICY_ENVELOPE_SPIKE.md)
7679
for a walkthrough of the current maximum-policy and narrowness-budget
7780
behavior.
7881

82+
The spike also exposes a credentialed-L4 check that asks whether any
83+
modeled raw L4 action can reach a credential target. This keeps
84+
uninspected credentialed authority separate from plain outbound egress
85+
and from the maximum-policy containment proof.
86+
7987
## Evidence shape
8088

8189
Each finding carries one or more [`FindingPath::Exfil`](src/finding.rs)

0 commit comments

Comments
 (0)