Skip to content

[Hackathon] cybersec-blackhat: dpop_jwt auth plugin + security validators#9

Open
mariagorskikh wants to merge 2 commits into
mainfrom
hackathon/cybersec-blackhat-dpop-auth
Open

[Hackathon] cybersec-blackhat: dpop_jwt auth plugin + security validators#9
mariagorskikh wants to merge 2 commits into
mainfrom
hackathon/cybersec-blackhat-dpop-auth

Conversation

@mariagorskikh

Copy link
Copy Markdown
Collaborator

Persona

cybersec-blackhat — senior security researcher. I look at every API and ask "how do I break this?" first, then "how do I harden it?" Auth, identity, trust layers are home turf.

Piece I picked

Layer 5 — Auth. Plus a new security validator module that can be applied to traces from any scenario.

The default jwt plugin is honestly described in the README as "HMAC-SHA256 token; not RFC JWT." Reading the code adversarially, it's worse than that label suggests for a multi-agent simulator that explicitly invites Byzantine peers:

  • No aud — a token meant for the registry can be replayed against payments.
  • No jti — perfect replay attacks; revocation requires storing entire token strings.
  • Custom payload|sig format — not actually a JWT.
  • Bearer-only — capturing the token is the attack; there is no proof-of-possession.
  • No iss — multi-tenant confusion.
  • No nbf, no clock-skew tolerance.

In a swarm that ships these tokens over in_memory transport, every other agent in the same process can scrape them out of the event queue.

Core idea

Two focused additions, no breakage:

1. auth: dpop_jwtDpopAuth plugin

A dependency-free, stdlib-only hardened auth implementation:

  • Real RFC-7519 layout: base64url(header).base64url(payload).base64url(sig).
  • Algorithm pinning to HS256. alg: none, alg: RS256 confusion, and unknown algs are rejected before the MAC check — kills the entire alg family of JWT bugs.
  • Audience binding (aud). verify_for_audience(token, audience=...) refuses tokens whose aud does not match. A token issued for registry cannot be replayed against payments.
  • Unique jti + per-verifier replay cache, bounded by token expiry (lazy GC + capacity cap, so it can't be flooded).
  • DPoP-style proof-of-possession. Tokens can be bound to an agent's identity public key via cnf.jkt. Verification then requires a fresh DpopProof (audience + this-token's-jti + iat, signed with the bound key). Stealing the token alone is not enough; the attacker also needs the key.
  • iss + nbf + configurable clock skew.
  • Revocation by jti (compact and bounded), with raw-token fallback so the bare Auth protocol's revoke(token) still works.
  • Deterministic given a seeded RNG — composes with NEST's replay-deterministic simulator.

Registered as ("auth", "dpop_jwt") in PluginRegistry. Scenarios opt in with auth: dpop_jwt.

2. nest_core.security_validators — trace-level security checks

A new validator module generic over auth-related trace events. Any plugin or scenario that emits auth.* events (shape documented in the module docstring) becomes inspectable for:

  • no_token_replay — same jti accepted twice by the same verifier.
  • audience_bindingpresented_aud != token.aud.
  • subject_matches_sendersub != claimed sender on the hop.
  • no_expired_acceptanceverify_success at t with exp < t.
  • dpop_binding_when_required — configurable per-audience policy flagging unbound bearer tokens for high-security audiences.

Validators degrade gracefully on missing fields, so existing traces (which don't emit auth events yet) get zero false positives.

How to test

uv sync
uv run pytest packages/nest-plugins-reference/tests/test_dpop_auth.py -v        # 33 tests
uv run pytest packages/nest-core/tests/test_security_validators.py -v           # 16 tests
uv run pytest                                                                   # 308 tests, all green
uv run ruff check . && uv run ruff format --check . && uv run pyright           # all clean
uv run nest doctor                                                              # 7/7

The plugin shows up via the registry:

uv run python -c "from nest_core.plugins import PluginRegistry; print(PluginRegistry().list_plugins('auth'))"
# [('auth', 'dpop_jwt'), ('auth', 'jwt')]

The test files are written as adversarial vignettes — each test names an attack that succeeds against the baseline JwtAuth and shows that DpopAuth blocks it. Two tests (test_baseline_jwt_has_no_audience_concept, test_baseline_jwt_accepts_replays) keep the original plugin honest by demonstrating exactly what it doesn't defend against.

Key assumptions

  • HMAC-SHA256 with a shared secret is the appropriate symmetric option for NEST's deterministic, in-process simulation. Asymmetric signing (Ed25519, RSA-PSS) is a natural next step but requires either a new dep (cryptography) or extending the existing Identity plugin to expose generic sign/verify; I kept this PR stdlib-only.
  • The DPoP proof uses HMAC over a canonical signing input with the agent's public-key bytes as the symmetric key. This is sufficient to bind a token to a specific key fingerprint in a simulation and is deterministic for replay tests; in production you'd use real asymmetric DPoP per RFC 9449.
  • The replay cache is per-DpopAuth instance. Operators who want cross-verifier replay protection wire their verifiers to a shared instance; operators who want strict per-audience isolation use one per audience. Both shapes are intentional.
  • Validator schema for auth.* trace events is documented in security_validators.py and meant to be a public contract for plugin authors who want to feed the validators.

Future work

  • Asymmetric DPoP via the existing Identity plugin (sign DPoP proofs with the agent's did:key rather than HMAC).
  • A small AuthLogger mixin that emits auth.* events into the trace so the new validators can run end-to-end on the built-in scenarios.
  • Switch one of the built-in scenarios (e.g. marketplace) to dpop_jwt and add a Byzantine "token replayer" agent — concrete demo of the validator catching real misuse.
  • Token introspection (RFC 7662) endpoint for verifiers that want a central revocation oracle.
  • Property-based tests with Hypothesis covering arbitrary claim mutations.

https://claude.ai/code/session_01C5j2D4MgCkPgsjSCqBVpWW


Generated by Claude Code

claude added 2 commits May 26, 2026 18:57
…e PoP

The default jwt plugin is a deliberately toy HMAC blob with no audience,
no jti, no proof-of-possession, and a custom payload|sig format. In a
multi-agent swarm that ships bearer tokens over the in-memory transport,
any observer can replay any token against any service that shares the
secret until expiry.

dpop_jwt is an opinionated, dependency-free hardened replacement:

  - RFC-7519-shaped (header.payload.sig, base64url).
  - HS256 only; alg=none and alg confusion rejected before the MAC check.
  - aud, iss, iat, nbf, exp, jti enforced; clock-skew leeway tunable.
  - jti replay cache scoped per verifier, bounded by token expiry.
  - Optional cnf.jkt binding to an agent's identity public key.
    Verification then requires a fresh DPoP proof (audience + jti +
    iat, signed with the bound key).
  - Revocation by jti, with raw-token fallback for Auth-protocol compat.

Registered as ('auth', 'dpop_jwt') in PluginRegistry so scenarios can
opt in via auth: dpop_jwt.

33 tests cover adversarial vignettes: alg=none, tampered payload, cross-
audience replay, jti replay, expired/future tokens, wrong issuer, DPoP
key mismatch, jti rebinding, audience swap on the proof, expired/future
proofs, tampered proof signatures, malformed inputs, determinism under
seeded RNG, and Auth-protocol conformance via runtime isinstance check.
A generic-over-trace-events validator module that flags the classic
adversarial patterns when a scenario emits auth.* events into its
trace stream. The validators are conservative: when a field is absent
they return PASS, so existing scenarios (which do not emit auth events
yet) are not affected.

Checks:
  - no_token_replay: same jti accepted twice by the same verifier.
  - audience_binding: presented_aud != token's aud.
  - subject_matches_sender: token sub != claimed sender on the hop.
  - no_expired_acceptance: verify_success at t with exp < t.
  - dpop_binding_when_required: configurable per-audience policy that
    flags unbound bearer tokens for high-security audiences.

Plus an aggregate validate_security_events() and a JSONL trace loader.
16 tests cover both positive and negative paths and confirm non-auth
traces produce zero false positives.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @mariagorskikh, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants