Local JWT policy review
jwt-policy-review is a non-default, development-only scanner and CLI feature.
The local evaluator itself still performs no network operation and adds no target
request. The surrounding scan command still performs its ordinary authorized
web-review requests. The feature is not part of default, the seven-member
release-bundle, or the published v0.10.0-alpha.2 archives. Compilation alone
activates nothing.
Build a feature-specific development binary:
cargo build --locked -p termivar-cli --no-default-features \
--features jwt-policy-review
Select the review only with explicit web-review, one strict policy, one local
public JWK, and exactly one token source:
termivar scan <AUTHORIZED_EXACT_ROOT> \
--profile web-review \
--jwt-policy jwt-policy.toml \
--jwt-public-jwk public-key.jwk \
--jwt-token-file compact-token.txt \
--report-dir jwt-assessment
The token may instead come from --jwt-token-env ENV_VAR or
--jwt-token-stdin. Token bytes never belong in argv. The three token-source
options are mutually exclusive. The policy plus the closed public-JWK structure
and canonical 32-byte x/y coordinates are validated before the token is
read. Curve membership and signature validity are decided only during local
ES256 verification; failure remains unauthenticated with
local_signature_status=invalid. The original token, expected claim values, JWK
coordinates, input paths, and environment-variable name are not saved in the
audit or ordinary diagnostics.
The policy file is strict TOML with schema security.jwt-local-policy/v1 and
these fields. Intended typ, issuer, and audience are mandatory V1 bindings;
none of the three checks can be disabled:
schema = "security.jwt-local-policy/v1"
policy_reference = "owned-fixture"
policy_revision = "owned-fixture-v1"
expected_type = "JWT"
expected_issuer = "https://issuer.example.invalid/"
expected_audience = "termivar-fixture"
required_claims = ["sub"]
require_expiration = true
allowed_clock_skew_seconds = 30
policy_reference and policy_revision are bounded, non-secret operator
labels. The revision must change whenever the private expected-value or
required-claim policy changes; Termivar cannot derive a publicly comparable
policy digest without exposing a dictionary-testable identifier for those
private values. Reports also include a SHA-256 identifier over the exact
supplied public P-256 point. That public-key identifier detects changed key
bytes but does not authenticate the key, its owner, or its source.
V1 accepts only a compact JWS whose protected header and claims fit the fixed
parser bounds and whose algorithm is exactly ES256. The local JWK must be an
EC/P-256/ES256 public key containing exactly kty, crv, alg, x, and y.
This structural admission does not establish that the coordinates are on the
P-256 curve before verification.
Private key material, embedded keys, jku, x5u, JWE, nested or compressed
JOSE, critical headers, unencoded payloads, unsecured algorithms, and other
algorithms are rejected or unsupported. Duplicate JSON keys and structural or
byte-limit overflows fail closed.
The audit keeps four states separate:
- parsing says only whether the token belongs to the supported compact profile;
- policy consistency says only whether the mandatory intended
typ, issuer and audience bindings plus the selected local claim/time rules held; - local signature status says only whether ES256 verification succeeded against the explicitly supplied local public key;
- target acceptance is
not_performedwhen the separate target-acceptance feature and option are absent.
A parsed token is not thereby signature-verified. A locally verified signature
does not authenticate the issuer or source, establish that a server accepts the
token, or establish authorization. The local evaluator never retrieves a remote
key or adds a target request. Without the separately compiled target-acceptance
feature and explicit --jwt-target-acceptance-policy, it never forwards or
replays the token. Policy, public-JWK and token files must be local regular files;
Windows UNC, device and named-pipe
namespaces are rejected before open. Mapped drives, mounted network filesystems
and mutable trusted-parent paths are not remotely distinguishable by this
lexical guard, so the operator remains responsible for supplying a trusted local
parent. Exploit execution and impact validation are not performed. The
evaluation clock is the local system clock and is not independently
authenticated.
Reports use security.jwt-policy-review-audit/v1 and contain only bounded,
value-free status, methodology, policy-violation class, and zero-network facts.
The strict saved-report reader and semantic comparison keep parsing, local
policy, local signature, target acceptance, methodology, and coverage changes
distinct. Methodology comparison includes the declared policy revision and the
actual public-key-byte identifier. Offline Verify checks bundle integrity; it
does not authenticate the token, key, issuer, source, or target claim.
Optional bounded target acceptance
The separate jwt-target-acceptance-review feature includes the local evaluator
but remains outside default, the seven-member release-bundle, and published
alpha.2 archives. Build a feature-specific development binary with:
cargo build --locked -p termivar-cli --no-default-features \
--features jwt-target-acceptance-review
Selection adds one non-secret local target policy to the complete local JWT inputs:
termivar scan <AUTHORIZED_EXACT_ROOT> \
--profile web-review \
--jwt-policy jwt-policy.toml \
--jwt-public-jwk public-key.jwk \
--jwt-token-file compact-token.txt \
--jwt-target-acceptance-policy target-acceptance.toml \
--report-dir jwt-target-assessment
The target policy is strict TOML. Its application must equal the selected
exact root, while resource must be query-free, fragment-free, exact-origin and
segment-contained by that application. Both URL spellings must already be
canonical ASCII: normalization-changing spellings, percent escapes,
backslashes, dot segments, default-port aliases and surrounding whitespace are
rejected before they can become request authority:
schema = "security.jwt-target-acceptance-policy/v1"
policy_reference = "owned-fixture-target"
policy_revision = "owned-fixture-target-v1"
application = "https://owned.example.invalid/app/"
resource = "https://owned.example.invalid/app/api/protected-marker"
resource_reference = "protected-marker"
success_json_field = "allowed"
The declared target policy revision (policy_revision) must change whenever the
application, resource, resource reference, or private success-marker semantics
change. Reports compare that revision because the URL and marker are
intentionally omitted; reusing a revision after changing those private semantics
would make two different methods appear comparable.
This policy grants one exact bodyless GET; it cannot nominate remote keys,
arbitrary discovered endpoints, a different application, a query, or a write.
Production credential use requires HTTPS. Numeric-loopback HTTP is admitted only
for owned fixtures. The policy, public key and token are validated/read through
the existing guarded local-input sequence: the non-secret policy and public key
are prepared before report output reservation; the secret token is read once
only after reservation. Invalid target policy therefore fails before the token
or output is acquired, while an invalid token aborts the reserved bundle safely.
Only a locally parsed, policy-consistent and ES256 signature-verified token can enter the move-only runtime handoff. Otherwise a selected zero-request audit records the appropriate local prerequisite as not established. Eligible runs use six fixed sequential roles:
- valid-token candidate;
- anonymous candidate;
- invalid-signature candidate;
- valid-token replay;
- anonymous replay;
- invalid-signature replay.
Only the invalid-signature roles are active. Each is admitted only after the valid role at the same stage observed the private marker and the corresponding anonymous role did not. Every role uses a fresh ambient-proxy-free pool with no cookie store, redirect, retry, or default credential. All roles share the parent exact-origin scope, request and active-verification accounting, response-byte accounting, cancellation, deadline and evidence authority. The child allows at most six requests, of which at most two are active, and one in flight at a time. Its local deadline is 30 seconds or the smaller remaining parent deadline.
Each response retains/interprets at most 64 KiB, with a 256 KiB aggregate
retention ceiling. The broker charges a complete delivered chunk before the
child decides how much can be retained, so accounted_response_bytes can exceed
retained_response_bytes by one final chunk overrun. The exact charged value is
reported separately; no overrun bytes are interpreted or relabelled as retained.
Only complete, committed, JSON-compatible status-200, 401 or 403 responses with
one strict top-level boolean marker can be classified. Status 200 alone, a login
page, wrong media type, partial body, duplicate JSON key, non-boolean field or
transport success alone is insufficient.
The saved local audit moves to security.jwt-policy-review-audit/v2 only when
this option is selected and then nests a strict
security.jwt-target-acceptance-audit/v1. The target audit stores the safe
operator policy/revision and resource references, ordered role/accounting facts,
opaque evidence references and a conservative relationship. It never stores the
token, Authorization value, selected JSON field, raw URL or response body. An
invalid-signature control that stably observes the marker while the anonymous
control does not is evidence only of that target relationship. It is not issuer
or source authentication, an installed privilege claim, an authorization-bypass
proof, exploit execution, impact validation or a Confirmed finding. Offline
Verify remains integrity-only, and Compare treats local method, target policy,
coverage and outcome changes separately.
Primary development references
These references define the reviewed development contract; they are not scan-time fetch targets and do not authorize any runtime network request: