Skip to content
dsh.fish
Bundle

dsh-tool-jwt

DSH JWT tool: HS256 decode (header/payload/expiry status), sign, and verify — zero runtime dependencies, RFC 7515-verified

Source
chenxuhl
License
MIT
Updated
Updated 22 hours ago

Readme

# dsh-tool-jwt

[English](README.en.md)

DSH JWT 工具插件 —— HS256 JWT 的**解析(不验签)**、**签发**与**完整验签**。纯函数、零运行时依赖(仅 Node 内置 `crypto`)。

[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

## 动机

后端开发日常:拿到一个 token 想看里面是什么、过期没有,但手算 base64url 解码 + JSON 解析 + 时间戳换算太慢;联调时需要造测试 token(网关/下游鉴权),手拼 header/payload/signature 容易错;验证 token 签名和有效期时,心算 HMAC 不可靠。

本插件提供确定性 JWT 工具,其中 `decode` 是差异化能力:**不验签**,只做结构解析 + 过期状态计算(当前时钟对比),用于快速排查 token 内容。`verify` 走完整校验链:算法检查(`alg=HS256`)、常数时间签名比较(`crypto.timingSafeEqual`,抗时序攻击)、过期判定(可选 `leewaySeconds` 时钟偏差容忍)。

## 安全模型

JWT 的签名伪造和算法混淆是真实威胁。防线:

1. **算法强制**:`verify` 仅接受 `alg=HS256`;`alg=none` 或任何非 HS256 直接拒绝,杜绝算法降级攻击
2. **常数时间签名比较**:`crypto.timingSafeEqual`,签名字节逐位比较不泄露时序信息
3. **输入长度上限**:token ≤ 16KB、secret ≤ 4KB、payload 序列化后 ≤ 8KB——超限在入口直接拒绝,不进入处理
4. **decode 不验签**:只做结构解析 + 过期状态计算,明确告知调用方"decode does not verify the signature"

> ⚠️ 本工具面向**开发/联调场景**(解码排查、造测试 token、验签)。不要把生产环境密钥粘贴到不可信会话。

其余边界:无效 base64url 直接报错(含输入截断);非 JSON header/payload 报错(含定位);非对象 JSON 报错;`exp` 非数字报错;secret 互斥检测(`secret` 与 `secretBase64url` 同时给即报错)。

## 工具声明

注册 `jwt` 工具(`dsh-tool-jwt`,row id `tool-jwt`),统一输出文本报告。

| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `action` | string | ✅ | `decode` / `sign` / `verify` |
| `token` | string | | JWT 紧凑序列化(`header.payload.signature`)。decode/verify 必需 |
| `payload` | object | | 声明对象(sign 必需),如 `{"sub":"u1","role":"admin"}`。`exp`/`iat` 由 `expiresInSeconds` 管理 |
| `secret` | string | | HMAC 密钥(UTF-8 字节)。sign/verify 必需。与 `secretBase64url` 互斥 |
| `secretBase64url` | string | | HMAC 密钥(base64url 编码的原始字节,用于二进制密钥如 RFC 7515 向量、JWK `k` 值)。与 `secret` 互斥 |
| `expiresInSeconds` | integer | | sign only:有效期秒数,设置 `exp = now + N`(`payload.exp` 已给时跳过) |
| `leewaySeconds` | integer | | verify only:允许的时钟偏差秒数(默认 0) |

## Actions

| action | 功能 | 输出示例 |
|---|---|---|
| `decode` | 解析 header/payload 为 JSON,按当前时钟计算过期状态(`valid` / `expired` / `no-exp-claim`,含 ISO 时间与剩余/已过期秒数)。**不验签** | `header: {"alg":"HS256","typ":"JWT"}`<br>`payload: {"sub":"u1","exp":1767231600}`<br>`expiry.status: valid`<br>`expiry.expiresInSeconds: 3600` |
| `sign` | 用 HS256 签发 token。可选 `expiresInSeconds` 自动填 `exp`,`iat` 自动填充;`payload.exp` 已给时跳过 | `token: eyJ...`<br>`claims: {"sub":"u1","iat":1767225600,"exp":1767231600}` |
| `verify` | 完整校验:`alg=HS256`、常数时间签名比较、过期判定(可选 `leewaySeconds`) | `valid: true`<br>`payload: {"sub":"u1","exp":1767231600}`<br>`expiry.expiresAt: 2026-01-01T01:00:00.000Z` |

## 示例

```
jwt { action: "decode", token: "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1MSJ9.sig" }
  → header: {"alg":"HS256","typ":"JWT"}
    payload: {"sub":"u1"}
    expiry.status: no-exp-claim
    note: decode does not verify the signature

jwt { action: "sign", payload: {"sub":"u1","role":"admin"}, secret: "topsecret", expiresInSeconds: 3600 }
  → token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1MSIsInJvbGUiOiJhZG1pbiIsImlhdCI6MTc2NzIyNTYwMCwiZXhwIjoxNzY3MjI5MjAwfQ.<sig>
    claims: {"sub":"u1","role":"admin","iat":1767225600,"exp":1767229200}

jwt { action: "verify", token: "eyJ...", secret: "topsecret", leewaySeconds: 30 }
  → valid: true
    payload: {"sub":"u1","role":"admin","iat":1767225600,"exp":1767229200}
    expiry.expiresAt: 2026-01-01T01:00:00.000Z
```

## 边界行为

| 情况 | 处理 |
|---|---|
| 非三段的 token | `jwt: token must have exactly three segments` |
| 非 JSON header/payload | `jwt: header/payload is not valid JSON` |
| 非对象的 header/payload | `jwt: header/payload JSON must be an object` |
| 无效 base64url | `jwt: invalid base64url input: "..."` |
| `exp` 非数字 | `jwt: exp claim must be a number` |
| `exp` 正好等于当前时间 | `expired`(RFC 7519:`exp` 必须**严格大于**当前时间) |
| 无 `exp` 声明 | `no-exp-claim` |
| token 超 16KB / secret 超 4KB / payload 超 8KB | 入口拒绝(不截断) |
| `secret` 与 `secretBase64url` 同时给 | 入口拒绝 |
| 签名不匹配 | `valid: false` + `reason: bad-signature` |
| `alg` 非 HS256 | `valid: false` + `reason: wrong-alg` |
| 过期 token | `valid: false` + `reason: expired`(`leewaySeconds` 内可容错) |
| 空 secret | `jwt: secret must be a non-empty string` |
| RFC 7515 二进制密钥 | 通过 `secretBase64url` 传入原始字节(`Buffer.from(key, 'base64url')`) |

## 正确性

签名逻辑由 [RFC 7515](https://www.rfc-editor.org/rfc/rfc7515) Appendix A.1 官方 HS256 测试向量验证——`tests/jwt-sign.spec.ts` 中 `verifyJwt: RFC vector` 测试组确认本实现与 RFC 标准输出逐字节一致。

## 安装

### Profile Bundle(推荐)

将本插件作为独立 bundle 安装到 profile:

```sh
# 交互式(web)profile —— 从 GitHub 仓库安装
dsh plugin --profile web add github:chenxuhl/dsh-tool-jwt
# 一次性任务(headless)profile —— dsh run 默认使用 headless
dsh plugin --profile headless add github:chenxuhl/dsh-tool-jwt
```

或从 `npm pack` 生成的 tarball 安装:

```sh
npm pack     # 生成 dsh-tool-jwt-<version>.tgz
# 交互式(web)profile
dsh plugin --profile web add ./dsh-tool-jwt-<version>.tgz
# 一次性任务(headless)profile
dsh plugin --profile headless add ./dsh-tool-jwt-<version>.tgz
```

包内 `dsh.bundle.patch` 会在安装后自动把插件加入 profile 的 layer stack(row id:`tool-jwt`)。插件缺失的 peer 依赖(`@deepseek-ai/cordis`、`@deepseek-ai/dsh-tools`)由 profile 的 healed `profiles/node_modules` 回退安装提供。

> ⚠️ web 与 headless 是**不同 profile**:web 安装不会自动覆盖 headless;`dsh run` 默认使用 headless profile。Windows 路径使用正斜杠(`C:/...`)。

### 本地开发(link 安装)

```sh
git clone https://github.com/chenxuhl/dsh-tool-jwt.git
cd dsh-tool-jwt && npm install && npm run build
dsh plugin --profile web add link:<repo-path>
```

> Windows + pnpm `link:` 协议注意:部分 pnpm 版本会把 `link:D:\...` 反斜杠路径错误拼接。使用正斜杠 `link:D:/...`;若 junction 仍指向错误路径,手动 `New-Item -ItemType Junction` 修复 `node_modules` 内的链接即可。

### 验证安装

```sh
dsh --profile web --dump-config | grep tool-jwt
```

### 运行验证

```sh
dsh run "用 jwt 工具 decode 一下 eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1MSJ9.sig"
```

## 测试

```bash
npm test
```

- `base64url.spec.ts`:编解码全分支 + 非法字符 + 非字符串输入 + 大小写/填充变体
- `jwt-decode.spec.ts`:三段拆分 + JSON 解析 + 过期状态(valid/expired/no-exp-claim/边界) + 大小守卫
- `jwt-sign.spec.ts`:RFC 7515 A.1 官方 HS256 向量 + 自签 round-trip + 错密钥 + 过期 + leeway 容错 + 篡改检测 + 非 HS256 算法拒绝
- `register.spec.ts`:注册契约(AUDIT-CROSS-02 风格)

## 许可

MIT

Install

dsh plugin --profile web add github:chenxuhl/dsh-tool-jwt#cf072741d9f2d8d9b4cd4999b174306f7969aeed

Profile: web

  • This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
Source