忘机山人
在第五章,我们已经确认了一件事:
> **只要 ENV 查看器能拿到懒猫 SSO 的公钥,
> 并成功验证签名,
> 那它就可以确信:
> 这些声明只能来自懒猫 SSO。**
但这句话里,隐藏着一个**极其危险的前提**:
> **ENV 查看器拿到的“那把公钥”,
> 真的是懒猫 SSO 的公钥。**
如果这一点站不住脚,
前一章所有关于签名、私钥、不可伪造的结论,都会瞬间失效。
于是,一个新的问题出现了:
> **ENV 查看器从哪里拿到公钥?
> 又凭什么相信这把公钥是对的?**
这正是 **JWKS** 存在的原因,也是懒猫 SSO 为你暴露 `https://alice.heiyu.space/sys/oauth/keys` 的意义。
---
## 6.1 一个天真的、但非常危险的想法
在最早期的懒猫玩法(以及很多自建 OIDC 的教程)里,你可能见过这样一种做法:
> “把懒猫 SSO 的公钥下载下来,写在应用的配置文件里。”
从功能上看,这当然可行。
但从系统设计上看,这是一个**不可接受的方案**。
原因并不复杂。
---
## 6.2 为什么“写死公钥”在懒猫里行不通
### 第一:密钥一定会轮换
懒猫官方会定期做 **Key Rotation**:
* 懒猫 SSO 的私钥定期更换
* 新旧密钥并存一段时间
* 逐步淘汰旧密钥
如果 ENV 查看器把公钥写死:
* 每次懒猫系统更新都需要同步更新
* 一旦漏更,就会导致**整个盒子的用户登录全部失败**
* 用户根本不知道发生了什么,只会觉得“这 App 又坏了”
这在工程上是不可接受的。
---
### 第二:懒猫 SSO 可能同时使用多把密钥
在真实的懒猫 SSO 实现中,签名者往往会:
* 使用不同密钥签发不同 token
* 在轮换过渡期内同时接受多把 key
* 为不同算法维护不同 key(懒猫目前只有 `RS256`,但未来可能变)
ENV 查看器如果只“认一把”,
就会在合法场景下误判 token 为非法。
---
### 第三:公钥不是“配置”,而是“动态信任关系”
最根本的问题在于:
> **公钥不是一个静态参数,
> 而是懒猫 SSO 对外发布的一部分“信任接口”。**
它应该:
* 可发现(通过 discovery)
* 可更新(懒猫升级后自动跟上)
* 可验证来源(绑定到 `iss`)
这三点,单靠配置文件是做不到的。
---
## 6.3 JWKS 是什么(准确但不绕)
JWKS(JSON Web Key Set)并不神秘。
它只是一个 **JSON 格式的公钥集合**,
通过 HTTPS 对外发布。
懒猫 SSO 的 JWKS 地址是:
```
https://alice.heiyu.space/sys/oauth/keys
```
这个地址**不是猜出来的**,它由 discovery 返回的 `jwks_uri` 字段决定:
```json
{
"issuer": "https://alice.heiyu.space/sys/oauth",
"jwks_uri": "https://alice.heiyu.space/sys/oauth/keys",
...
}
```
一个典型的返回看起来像这样:
```json
{
"keys": [
{
"kty": "RSA",
"kid": "3e7f...",
"use": "sig",
"alg": "RS256",
"n": "…",
"e": "AQAB"
}
]
}
```
每一项,描述的都是一把 **可用于验签的公钥**。
---
## 6.4 JWKS 解决的不是“有没有公钥”,而是三个更深的问题
### 第一:**发现(Discovery)**
ENV 查看器不需要提前知道公钥内容。
它只需要知道:
> **“去哪里拿。”**
这个“哪里”,不是随意指定的 URL,
而是通过 OIDC Discovery 机制获得的:
```
GET https://alice.heiyu.space/sys/oauth/.well-known/openid-configuration
```
在这里,懒猫 SSO 明确告诉 ENV 查看器:
* 我是谁(`issuer`)
* 我的 token endpoint 在哪(`token_endpoint`)
* **我的公钥在哪**(`jwks_uri`)
而这个 issuer 的值,又由你容器里的 `LAZYCAT_AUTH_OIDC_ISSUER_URI` 环境变量决定——
懒猫注入的这个变量,就是整个信任链的**起点**。
---
### 第二:**选择(Selection)**
JWT 的 header 中,有一个非常重要的字段:
```json
{ "alg": "RS256", "kid": "3e7f..." }
```
`kid` 的含义只有一个:
> **“这个 token 是用哪一把 key 签的。”**
ENV 查看器在拿到的 JWKS 里:
* 查找 `kid` 对应的 key
* 用这把 key 验签
这使得:
* 多 key 并存成为可能
* key 轮换不需要盒子重启、也不需要 App 重部署
---
### 第三:**更新(Rotation)**
JWKS 是一个**可随时间变化的集合**:
* 懒猫升级引入新 key
* 旧 key 保留一段时间
* ENV 查看器通过缓存 + 刷新机制逐步更新
实现层面的建议(用主流 OIDC 库几乎都自带):
* 缓存 JWKS 几分钟到几小时
* 遇到没见过的 `kid`,主动刷新一次
* 刷新还找不到 → 拒绝 token,不要无限循环
这让“长期运行的信任关系”成为可能——
你的 App 写好一次,懒猫这边换多少次 key 都不用动它。
---
## 6.5 Client 是如何使用 JWKS 的(流程级)
现在我们把 JWKS 放回到实际流程中。
当 ENV 查看器拿到一个 ID Token 时,它会:
1. 解析 JWT header
2. 读取 `alg`(必须是 `RS256`)、`kid`
3. 从缓存的 JWKS 中查找对应公钥
4. 如果找不到:
* 重新拉取 `https://alice.heiyu.space/sys/oauth/keys`
* 再次查找
5. 如果仍然找不到:
* **拒绝这个 token**
注意这个结论:
> **“找不到对应公钥”
> 本身就是一个拒绝信号。**
如果你的 App 在这里写了 fallback(“找不到就跳过签名验证”),
那整个懒猫 SSO 的安全模型在你这里就破了。
---
## 6.6 为什么“中间人伪造 JWKS”行不通
这是一个经常被提出、也非常值得认真回答的问题:
> “如果有人在家庭网络里拦截到 `/sys/oauth/keys` 的请求,
> 返回一套伪造的公钥,会怎样?”
这个攻击模型**并不成立**,原因不在于 JWKS 本身,而在于:
> **JWKS 从来不是“裸奔”的。**
---
### 6.6.1 HTTPS 是第一道信任根
JWKS 是通过 HTTPS 获取的,而且 `heiyu.space` 的证书是懒猫官方签发的可信证书。
这意味着:
* ENV 查看器会验证 TLS 证书
* 会验证域名
* 会验证证书链
要成功伪造 JWKS,中间人必须:
* 控制 DNS
* 拥有可信 CA 签发的 `*.heiyu.space` 证书
这已经超出了懒猫 SSO 的威胁模型。
> ⚠️ 例外:如果你手贱在 App 里关掉了 TLS 验证(比如用自签证书做开发调试),这条就不成立了。**别在生产配置里关 TLS 验证。**
---
### 6.6.2 issuer 绑定是第二道闸门
ENV 查看器并不是“随便拉一个 JWKS”。
它的逻辑是:
> **“这个 JWKS 属于 `https://alice.heiyu.space/sys/oauth` 这个 issuer。”**
而 issuer 本身,已经在 discovery 阶段被固定(来自 `LAZYCAT_AUTH_OIDC_ISSUER_URI`)。
攻击者即使伪造一套 JWKS,也无法绕过:
* `iss` 校验
* `aud` 校验
* discovery 绑定
---

### 6.6.3 公钥可以公开,但私钥无法伪造
这是整个模型中最容易被忽略的一点:
> **安全性不来自“公钥保密”,
> 而来自“私钥不可得”。**
攻击者可以:
* 复制懒猫 SSO 的公钥
* 重发它的 JWKS
* 声称“这是我的 key”
但只要他没有懒猫 SSO 的私钥:
> **就无法生成任何能通过验签的 token。**
---
## 6.7 一个非常重要的实现原则(必须强调)
ENV 查看器在实现 JWKS 支持时,必须遵守一个原则:
> **永远不要从 token 本身“学会”信任来源。**
具体来说:
* ❌ 不要信任 token header 里的 `jku`
* ❌ 不要信任 token header 里的 `x5u`
* ❌ 不要允许 token 指定“去哪拉公钥”
* ❌ 不要把 `jwks_uri` 做成“App 用户可配置的”(这是一个经典反面教材)
JWKS 的来源,必须是:
> **懒猫注入的 `LAZYCAT_AUTH_OIDC_ISSUER_URI` → discovery → `jwks_uri`**
这是防止 **Key Injection** 的关键。
---
## 6.8 到这里,我们真正拥有了什么
在本章结束时,ENV 查看器已经具备了三项能力:
1. 能验证 token 的签名
2. 能确信使用的是懒猫 SSO 发布的公钥
3. 能适应密钥轮换而不中断服务
这意味着:
> **ENV 查看器已经能确认:
> “这组声明确实来自这台盒子的懒猫 SSO,且没有被篡改。”**
但请注意:
> **我们仍然没有回答一个更关键的问题。**
---
## 6.9 合法的声明,仍然可能是错误的声明
一个 token 可以:
* 签名完全正确
* 公钥完全可信
但仍然可能:
* 发给盒子上**别的** App(`aud` 是 `some.other.app`)
* 用在错误的场景
* 来自错误的 issuer(你接入了多个 OIDC,比如 Casdoor)
* 已经过期
* 被重放
也就是说:
> **“来源可信”
> ≠
> “现在就该信任并使用这些声明”。**
---
## 6.10 本章结论(必须清晰)
我们可以把这一章的结论总结为:
1. 懒猫 SSO 的公钥通过 `https://.heiyu.space/sys/oauth/keys` 发布
2. JWKS 解决的是“公钥发现、选择与轮换”问题
3. ENV 查看器对公钥的信任,来自 HTTPS + `LAZYCAT_AUTH_OIDC_ISSUER_URI` 绑定
4. 公钥可以公开,懒猫 SSO 的私钥不可伪造
5. 签名验证通过,只是信任链的一部分

---
### 本章小结(一句话)
> **JWKS 让“数学可信”变成了“懒猫 App 可用的可信”。**
评论
0暂无评论