一、参数与风险
- 风险:伪造与重放、过期窗口过长、kid失配与密钥轮换不当。
- 基线:固定算法与头字段、严格时间戳窗口与唯一Nonce、kid映射公钥。
二、Base64URL与签名
```ts
import crypto from 'crypto'
function b64url(input: Buffer): string { return input.toString('base64').replace(/=/g,'').replace(/\+/g,'-').replace(/\//g,'_') }
function signRsaSha256(privateKeyPem: string, data: string): string {
const s = crypto.createSign('RSA-SHA256')
s.update(data)
s.end()
const sig = s.sign(privateKeyPem)
return b64url(sig)
}
function verifyRsaSha256(publicKeyPem: string, data: string, sigB64u: string): boolean {
const v = crypto.createVerify('RSA-SHA256')
v.update(data)
v.end()
const sig = Buffer.from(sigB64u.replace(/-/g,'+').replace(/_/g,'/'), 'base64')
return v.verify(publicKeyPem, sig)
}
```
三、头字段与数据格式
```ts
type Req = { headers: Record; rawBody: string }
type Res = { status: (n: number) => Res; end: (b?: string) => void }
function canonicalPayload(path: string, body: string): string {
return path + '\n' + body
}
```
四、Nonce窗口与kid映射
```ts
class NonceStore {
keep = new Set()
add(n: string) { this.keep.add(n) }
has(n: string): boolean { return this.keep.has(n) }
}
type KeyMap = Record
```
五、服务端校验
```ts
function nowSec(): number { return Math.floor(Date.now()/1000) }
function verifyApiSignature(req: Req, res: Res, keys: KeyMap, windowSec: number, nonces: NonceStore): boolean {
const kid = req.headers['x-kid'] || ''
const sig = req.headers['x-signature'] || ''
const ts = Number(req.headers['x-timestamp'] || '0')
const nonce = req.headers['x-nonce'] || ''
const pub = keys[kid]
if (!pub) return false
if (!Number.isFinite(ts)) return false
if (Math.abs(nowSec() - ts) > windowSec) return false
if (nonces.has(nonce)) return false
const data = canonicalPayload(req.headers['x-path'] || '/', req.rawBody)
const ok = verifyRsaSha256(pub, `${ts}.${nonce}.${data}`, sig)
if (!ok) return false
nonces.add(nonce)
return true
}
```
六、验收清单
- 固定算法与头字段:`X-Kid/X-Signature/X-Timestamp/X-Nonce/X-Path`;窗口≤`300`。
- `kid`映射到公钥并支持轮换;`nonce`唯一性通过;数据采用规范化格式。
- 验签失败拒绝并记录审计,包含`kid`与来源路径。
发表评论 取消回复