Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,33 @@ so the base app installer stays small and you only download what you actually us

Relevant commits: `1b59500` (Redis/RabbitMQ/Container Manager), `5ad6ba2` (Kafka
Explorer), and the Rust-side sidecar cutovers `9a76337`/`d777bb0`/`8566311`.

### JWT Debugger now verifies and signs

The tool decoded a token and stopped there. It now answers the two questions
people actually open it with — is this signature real, and can I mint one like
it — across every algorithm the app's webview can perform: **HS256/384/512,
RS256/384/512, PS256/384/512, ES256/384/512, EdDSA**, plus unsigned `none` for
testing how a server reacts to one.

- **Verify** takes the shared secret for `HS*`, or a PEM public key, an X.509
certificate, a JWK, or a whole JWK Set (the right key is picked by the
token's own `kid`).
- **Sign** takes a PKCS#8 private key or private JWK, an expiry like `1h` /
`7d`, and an optional `kid`. **Generate pair** creates a fresh key pair and
fills the public half in for the Verify side.
- **Decode** gained a claims table: `exp` / `nbf` / `iat` as real times with
"in 3 minutes" / "12 days ago", and an expired token says so.
- HMAC secrets carry an explicit encoding (plain text / base64 / base64url /
hex), because a secret from `openssl rand -base64 32` is *bytes* — signing
with its printed characters instead produces a token the real service
rejects.
- Verification never takes the algorithm from the token's own `alg` header
(that is the algorithm-confusion attack): you pick the algorithm you expect,
and a token that disagrees is reported as a mismatch. Expired or
wrong-audience results are shown as such rather than as a bad signature.

Everything runs locally through the OS webview's own Web Crypto — no token,
key or claim leaves the machine — and tokens and keys are held in the OS
secret store rather than ordinary app storage. Over MCP, `jwt_sign` and
`jwt_verify` join `jwt_decode`.
38 changes: 37 additions & 1 deletion src/components/mcpUtilityTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,45 @@ export const JWT_MCP_TOOLS: McpToolDef[] = [
// gated by the `jwt` tool id.
{
"name": "jwt_decode",
"description": "Decode a JWT's header and payload. Decode-only — does NOT verify the signature (no verification key available).",
"description": "Decode a JWT's header and payload, plus `expired`/`notYetValid` computed from exp/nbf. Does NOT check the signature — use jwt_verify for that.",
"inputSchema": { "type": "object", "properties": { "token": { "type": "string" } }, "required": ["token"] }
},
{
"name": "jwt_verify",
"description": "Verify a JWT's signature (and optionally iss/aud/sub) with a key you supply. `algorithm` is the one you EXPECT — the token's own `alg` header is never trusted to choose, so a token whose header disagrees is rejected with reason \"alg\". A failed check is a normal result, not an error: returns { valid, reason, message, header, payload }, where reason is one of format/key/alg/signature/expired/nbf/claim. `key` is the shared secret for HS*, or a PEM SPKI public key, X.509 certificate, JWK or JWK Set for the rest. `keyEncoding` says how an HS* secret is written down (utf8 default, base64, base64url, hex) — getting it wrong changes the bytes and fails the signature.",
"inputSchema": {
"type": "object",
"properties": {
"token": { "type": "string" },
"algorithm": { "type": "string", "enum": ["HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "PS256", "PS384", "PS512", "ES256", "ES384", "ES512", "EdDSA", "none"] },
"key": { "type": "string" },
"keyEncoding": { "type": "string", "enum": ["utf8", "base64", "base64url", "hex"] },
"issuer": { "type": "string" },
"audience": { "type": "string" },
"subject": { "type": "string" },
"clockTolerance": { "type": "number", "description": "Seconds of leeway on exp/nbf." }
},
"required": ["token", "algorithm", "key"]
}
},
{
"name": "jwt_sign",
"description": "Sign claims into a JWT. `key` is the shared secret for HS*, or a PEM PKCS#8 private key or private JWK for RS*/PS*/ES*/EdDSA (PKCS#1 \"BEGIN RSA PRIVATE KEY\" is not accepted — convert with `openssl pkcs8 -topk8`). `expiresIn`/`notBefore` take a duration (\"1h\", \"7d\", or plain seconds), never an absolute timestamp. `iat` is set unless issuedAt is false. Algorithm \"none\" produces an UNSIGNED token that proves nothing — only for testing how a server reacts to one.",
"inputSchema": {
"type": "object",
"properties": {
"payload": { "type": "object", "description": "Claims object. A JSON string is also accepted." },
"algorithm": { "type": "string", "enum": ["HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "PS256", "PS384", "PS512", "ES256", "ES384", "ES512", "EdDSA", "none"] },
"key": { "type": "string" },
"keyEncoding": { "type": "string", "enum": ["utf8", "base64", "base64url", "hex"] },
"expiresIn": { "type": "string" },
"notBefore": { "type": "string" },
"kid": { "type": "string" },
"issuedAt": { "type": "boolean" }
},
"required": ["payload", "algorithm"]
}
},
];

export const JSON_MCP_TOOLS: McpToolDef[] = [
Expand Down
93 changes: 0 additions & 93 deletions src/components/tools/JwtDebugger.tsx

This file was deleted.

Loading
Loading