Skip to content

jwt: sign and verify, every algorithm a webview can do - #166

Merged
DianaSensei merged 2 commits into
mainfrom
claude/container-red-logs-debug-i3zngu
Sep 23, 2026
Merged

DianaSensei merged 2 commits into
mainfrom
claude/container-red-logs-debug-i3zngu

Conversation

@DianaSensei

Copy link
Copy Markdown
Owner

Why

The JWT tool decoded a token and stopped there — the pane said so itself. That left unanswered the two questions people actually open a JWT tool with: is this signature real, and can I mint one like it to test my service.

What changed

New src/components/tools/jwt/jwtCrypto.ts does both on jose (already a dependency — it backs the API Client's jsonwebtoken shim), driving the OS webview's own Web Crypto. Nothing leaves the machine.

Algorithms

HS256/384/512 · RS256/384/512 · PS256/384/512 · ES256/384/512 · EdDSA · unsigned none

Availability is probed from the engine, not tabulated: Ed25519 landed in WebKit, Chromium and Gecko at different times and the same build ships to all three. Unavailable entries grey out in the picker instead of failing with "Unrecognized name" after the user has pasted a key.

Keys

Format Used for
PKCS#8 BEGIN PRIVATE KEY signing
SPKI BEGIN PUBLIC KEY verifying
X.509 BEGIN CERTIFICATE verifying
JWK both — one without d is refused for signing
JWK Set verifying, key chosen by the token's kid

PKCS#1 and SEC1 keys, which Web Crypto cannot import, are named as such along with the openssl pkcs8 -topk8 -nocrypt line that converts them, instead of failing obscurely.

HMAC secrets carry an explicit encoding (plain text / base64 / base64url / hex). A secret from openssl rand -base64 32 is bytes; HMAC-ing its 44 printed characters produces a token the real service rejects, with nothing on screen to say why.

Verification does not trust the token

Reading the algorithm off the alg header is the algorithm-confusion attack (an HS256 token handed to an RS256 verifier, signed with the RSA public key as the HMAC secret). You pick the algorithm you expect; a token that disagrees is reported as a mismatch. none decodes readably but never counts as verified.

Failure modes are distinguished rather than flattened into one red "invalid": expired, not yet valid and failed claim check are amber — the signature held, the problem is elsewhere.

Decode

Gained a registered-claims table: exp / nbf / iat as real times with "in 3 minutes" / "12 days ago". exp: 1758604262 never answered "is this expired?".

Elsewhere

  • Generate pair fills both halves, so Verify is ready the moment something is signed.
  • expiresIn takes 1h, 7d or plain seconds; a bare number means from now, not a 1970 timestamp (the trap jsonwebtokenShim.ts documents).
  • Tokens and keys go to the secret vault via useSecretState, never sdk.storage.
  • MCP gains jwt_sign and jwt_verify running the same module. A failed verification comes back as a result ({valid, reason, message, header, payload}) rather than a thrown error — the caller asked a question and "no, it expired" is the answer.

Testing

  • 56 new tests: a sign/verify round trip per algorithm, tampered payloads, algorithm confusion, clock tolerance, JWK Set selection by kid, every secret encoding, and the none cases. They run under the Node environment for the realm reason jsonwebtokenShim.test.ts documents.
  • npm test → 1452 passed · tsc --noEmit clean · npm run build clean.
  • Design guards (h-ctl control heights, focus-ring formula) green.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DvjATyCpgyTf5gzkhZ3nP9


Generated by Claude Code

DianaSensei and others added 2 commits September 23, 2026 07:22
The JWT tool decoded and stopped there — the pane even said so — so the
two questions people actually open it with went unanswered: is this
signature real, and can I mint one like it to test against my service.

New `jwt/jwtCrypto.ts` does both on `jose` (already a dependency, already
carrying the API Client's `jsonwebtoken` shim), driving the OS webview's
own Web Crypto. Nothing leaves the machine.

- Algorithms: HS256/384/512, RS256/384/512, PS256/384/512, ES256/384/512,
  EdDSA, plus unsigned `none` for testing how a server reacts to one.
  Availability is probed from the engine rather than tabulated, because
  Ed25519 landed in WebKit, Chromium and Gecko at different times and the
  same build ships to all three; unavailable entries grey out instead of
  failing after the user has pasted a key.
- Keys: PKCS#8 private, SPKI public, X.509 certificate, JWK, or a whole
  JWK Set (the key is picked by the token's own `kid`). PKCS#1 and SEC1
  keys, which Web Crypto cannot import, are named as such with the
  `openssl pkcs8 -topk8` line that fixes them.
- HMAC secrets carry an explicit encoding (text / base64 / base64url /
  hex). A secret from `openssl rand -base64 32` is bytes; HMAC-ing its
  printed characters produces a token the real service rejects, with
  nothing on screen to say why.
- Verify never reads the algorithm off the token's header — that is
  algorithm confusion, and a debugger that reproduced it would teach it.
  A mismatch is reported as a mismatch. "Expired" and "wrong audience"
  are shown as amber, not as a red forgery: the signature held.
- Decode gained the claims table — exp / nbf / iat as real times with
  "in 3 minutes" / "12 days ago", which is what "is this expired?"
  actually needs.
- Generate pair fills both halves, so the Verify side is ready the moment
  a token is signed.

Tokens and keys go to the secret vault (`useSecretState`), never
`sdk.storage`. `jwt_sign` and `jwt_verify` join `jwt_decode` over MCP,
running the same module, with a failed verification returned as a result
rather than thrown — the caller asked a question and "no, it expired" is
the answer.

56 new tests cover a round trip per algorithm, tampering, algorithm
confusion, clock tolerance, JWK Set selection, secret encodings, and the
`none` cases.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DvjATyCpgyTf5gzkhZ3nP9
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DvjATyCpgyTf5gzkhZ3nP9
@pullfrog

pullfrog Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
  • Checkout PR and read complete diff end-to-end
  • Triage: identify domain, seams, external contracts
  • Read surrounding code / callers / tests
  • Dispatch reviewfrog specialists for unresolved questions
  • Aggregate findings and draft review
  • Update PR summary snapshot
  • Submit review

Important

Pullfrog covered this run's model usage. DeepSeek Flash is fast and cheap — expect lighter work than a frontier model. This model allowance is temporary and separate from your Pullfrog plan. Connect a model-provider subscription or API key →

Pullfrog  | View workflow run | via Pullfrog | Using DeepSeek Flash (model usage covered by Pullfrog) | 𝕏

@sonarqubecloud

Copy link
Copy Markdown

Copy link
Copy Markdown
Owner Author

github-advanced-security is red on 6577cd1, and it is not this PR's.

The job never reached the diff — it failed while opening its own model session:

Error creating PR review request: SessionModelError: Execution failed:
CAPIError: 400 The requested model is not supported.
COPILOT_AGENT_MODEL: sweagent-capi:claude-opus-5[ReasoningEffort=medium]

That is a GitHub-side service/configuration problem in the code-scanning agentic reviewer, not a finding against the change. Everything the repo's own CI runs is green on this commit — Typecheck and unit tests, npm audit, cargo audit, Secret scan, Dependency review, Verify SECURITY.md claims, and SonarCloud's quality gate (0 security hotspots).

There is no fix to port from anywhere: nothing in the repository selects that model. I have re-run the failed job once; if it fails the same way again, the failure stands as an outage of that checker rather than a signal about this diff.


Generated by Claude Code

@codecov

codecov Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 45.51084% with 176 lines in your changes missing coverage. Please review.
✅ Project coverage is 42.75%. Comparing base (3a0deea) to head (6577cd1).

Files with missing lines Patch % Lines
src/components/tools/jwt/JwtDebugger.tsx 0.00% 98 Missing ⚠️
src/components/tools/jwt/jwtParts.tsx 0.00% 40 Missing ⚠️
src/components/tools/jwt/jwtCrypto.ts 78.70% 11 Missing and 22 partials ⚠️
src/components/tools/jwtMcpBridge.ts 86.20% 0 Missing and 4 partials ⚠️
src/plugins/jwt/plugin.ts 0.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #166      +/-   ##
==========================================
+ Coverage   42.67%   42.75%   +0.07%     
==========================================
  Files         300      302       +2     
  Lines       19917    20222     +305     
  Branches     4933     5049     +116     
==========================================
+ Hits         8500     8645     +145     
- Misses      10400    10535     +135     
- Partials     1017     1042      +25     
Flag Coverage Δ
frontend 38.12% <45.51%> (+0.18%) ⬆️
rust 65.43% <ø> (-0.06%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/components/mcpUtilityTools.ts 100.00% <ø> (ø)
src/lib/toolGuides.tsx 37.50% <ø> (ø)
src/plugins/jwt/plugin.ts 0.00% <0.00%> (ø)
src/components/tools/jwtMcpBridge.ts 85.71% <86.20%> (ø)
src/components/tools/jwt/jwtCrypto.ts 78.70% <78.70%> (ø)
src/components/tools/jwt/jwtParts.tsx 0.00% <0.00%> (ø)
src/components/tools/jwt/JwtDebugger.tsx 0.00% <0.00%> (ø)

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@DianaSensei
DianaSensei merged commit 24b4932 into main Sep 23, 2026
19 of 21 checks passed
@DianaSensei
DianaSensei deleted the claude/container-red-logs-debug-i3zngu branch September 23, 2026 08:08
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.

1 participant