# 懒猫SSO第五章 签名:Client 凭什么相信懒猫的这些声明

忘机山人

发布于27天前
博客图片修整中,看不了可以先搜索公众号“忘机山人”看。
在第四章,我们已经澄清了一件非常重要的事:



https://appstore.lazycat.cloud/#/shop/detail/xu.deploy.env


> **懒猫 ID Token 不是加密的。
> 它是一组“被懒猫 SSO 签名的声明”。**

这句话看起来很简单,但它隐含了一个巨大的问题:

> **如果任何人都能看到 `groups: ["admin"]`,
> ENV 查看器又凭什么相信这些声明是真的?**

换句话说:

> **签名到底在“证明”什么?**

这一章,我们就只回答这一件事。

---

## 5.1 从一句最容易被误解的话开始

在很多讨论中,你可能见过这样一句话:

> “懒猫 SSO 用私钥加密,Client 用公钥解密。”

这句话**在懒猫 SSO / OIDC 语境下是错误的**。

它把两件完全不同的事情混在了一起:

* **加密(Encryption)**
* **签名(Signature)**

如果你不在这里把这两个概念彻底分开,后面所有关于 JWT、JWKS(`/sys/oauth/keys`)、ID Token 校验逻辑的理解都会偏掉。

---

## 5.2 加密 vs 签名:这是两套安全模型

我们先用最严格、最不含糊的方式区分这两件事。

### 加密(Encryption)

目标只有一个:

> **让别人看不到内容**

典型模型是:

* 发送方用接收方的公钥加密
* 只有接收方能用私钥解密

这是 **保密性(Confidentiality)**。

---

### 签名(Signature)

目标完全不同:

> **证明“这是谁发的”,以及“中途有没有被改”**

典型模型是:

* 声明方(懒猫 SSO)用自己的私钥签名
* 任何人(ENV 查看器、Memos、Vaultwarden……)都可以用公钥验证

这是 **真实性(Authenticity)+ 完整性(Integrity)**。

---

### 懒猫 ID Token 用的是哪一个?

> **只用签名。**

懒猫 SSO 在 discovery 里写得明明白白:

```json
"id_token_signing_alg_values_supported": ["RS256"]
```

没有 `alg_values_supported` 里的加密算法字段,也没有 JWE 相关的任何东西。

懒猫的设计从一开始就**不试图隐藏内容**。

它只试图证明一件事:

> **“这组声明,确实是由 `https://alice.heiyu.space/sys/oauth` 这个懒猫 SSO 发出的,而且没有被修改。”**

---

## 5.3 签名要解决的核心问题

站在 ENV 查看器的角度,签名必须同时解决三个问题:

1. **来源问题**
   > 这些声明是不是**这台盒子的** 懒猫 SSO 发的?(不是别的盒子、不是自建的假 IdP)
2. **篡改问题**
   > 中途有没有人把 `groups: ["family"]` 改成 `groups: ["admin"]`?
3. **不可伪造问题**
   > 攻击者能不能自己造一个“看起来合法”的 token,直接塞给 ENV 查看器?

如果签名无法同时解决这三个问题,那整个懒猫 SSO 的安全模型就会崩塌——
毕竟所有 App 都信懒猫,懒猫只要一处可伪造,全盒子的应用都可以被绕过。

---

## 5.4 签名并不是“对内容做点处理”

我们来看一个懒猫 ID Token 的真实签名结构(`RS256`):

```
header.payload.signature
```

其中:

* `header.payload` 是**完全可读的**
* `signature` 是唯一的安全锚点

签名不是“把 payload 加密一下”,
而是一个**数学等式的结果**。

---

## 5.5 签名在数学上到底做了什么(不绕弯)

我们用最直白、但不偷懒的方式,把懒猫 ID Token 的签名过程拆开。

### Step 1:确定要被保护的数据

在 JWT 中,被签名的数据是:

```
base64url(header) + "." + base64url(payload)
```

注意三点:

1. 是 **编码后的字符串**
2. 顺序固定
3. 任意一个字符改变,结果都会变

---

### Step 2:对数据做 Hash(摘要)

`RS256` 里的 `256` 对应 SHA-256。

对这段字符串做一次哈希运算:

```
hash = SHA256(data)
```

哈希的意义只有一个:

> **把任意长度的数据,映射成一个固定长度、不可逆、对变化极其敏感的摘要。**

只要原始数据有 1 bit 变化,hash 就完全不同。

---

### Step 3:用懒猫 SSO 的私钥对 hash 进行签名

这一步才是真正的“签名”:

```
signature = RSA-Sign(hash, sso_private_key)
```

这里的关键点是:

* **只有懒猫 SSO(跑在盒子里的那个进程)**持有这个私钥
* 签名过程是单向的
* 没有这把私钥,无法伪造一个“能通过验证的 signature”

懒猫官方会在升级、Key Rotation 的时候轮换这把 key,但不会把它泄漏给任何 App。
如果你在 App 里“需要懒猫 SSO 的私钥”,那你已经做错了一件事。

---

### Step 4:Client 用公钥验证

ENV 查看器拿到 token 后,会做对称的操作:

1. 用同样的方式计算 `hash_client = SHA256(base64url(header) + "." + base64url(payload))`
2. 从 `https://alice.heiyu.space/sys/oauth/keys` 拉到的 JWKS 里,找到 header.`kid` 对应的公钥
3. 用公钥验证 `signature` 是否对应这个 hash

如果验证通过,数学上意味着一件事:

> **这个 signature 只能来自对应的私钥,
> 而这个私钥只在盒子里的身份服务 手里。**

---

## 5.6 为什么不能伪造签名

这是整个懒猫 SSO 信任链的核心。

攻击者即使:

* 完全拿到 header 和 payload(反正它们本来就明文)
* 完全知道算法(懒猫 discovery 里大大方方写着 `RS256`)
* 完全知道公钥(`/sys/oauth/keys` 对全世界公开)

他依然做不到一件事:

> **生成一个能通过验证的 signature**

因为 RSA 签名算法的安全性,建立在一个非常强的前提上:

> **从公钥推导私钥,在计算上是不可行的。**

这不是工程约定,而是现代密码学的基础假设。

---

## 5.7 为什么“改 1 个字符”就会彻底失败

这是懒猫 ID Token 安全性的第二个关键点。

如果攻击者尝试修改 payload 中的任何字段,比如把:

```json
"preferred_username": "alice",
"groups": ["family"]
```

改成:

```json
"preferred_username": "alice",
"groups": ["admin"]
```

会发生什么?

* `payload` 改变
* `base64url(header) + "." + base64url(payload)` 改变
* `hash` 改变
* 但 `signature` 没变(攻击者没有私钥重签)

结果只有一个:

> **签名验证失败。**

这保证了:

> **懒猫 SSO 声明一旦被签名,就不可被悄悄篡改。**

这也意味着:**你可以、并且应该信任 `groups` 字段**。只要签名验证通过,它就是懒猫原始发出的样子。

---

![image.png](https://dl.playground.lazycat.cloud/guidelines/459/2d1b5875-c4ec-493c-ab85-07bd2c898aa1.png "image.png")

## 5.8 签名解决的,只是“真实性”和“完整性”

到这里,我们必须再次强调签名的边界。

签名能证明:

* 声明来自**某个** OP
* 声明未被修改

但它**不能证明**:

* 声明是不是发给 `xu.deploy.env` 的(那是 `aud`)
* 声明是否已经过期(那是 `exp`)
* 声明是否对应你这次登录(那是 `nonce`)
* 声明是否被错误使用
* 声明来自的是**这台盒子**的懒猫 SSO 还是别的(那是 `iss`)

换句话说:

> **签名只能回答“这是真的吗”,
> 不能回答“这该不该信”。**

---

## 5.9 一个容易被忽略但致命的误解

很多懒猫 App 里会出现这种错误逻辑:

> “既然签名验证通过了,那 token 就是安全的,我就可以直接读 `preferred_username` 登录用户了。”

这是**不成立的**。

在懒猫 SSO 里:

* **签名验证是必要条件**
* **但远远不是充分条件**

你可以把它理解成:

> **签名只是“验明正身”,
> 而不是“授权使用”。**



## 5.10 本章结论(必须钉死)

我们用几条不可模糊的结论结束这一章:

1. 懒猫 ID Token 使用的是数字签名(`RS256`),而不是加密
2. 签名保证来源真实性和内容完整性
3. 懒猫 SSO 的私钥决定“谁能签”,公钥(发布在 `/sys/oauth/keys`)决定“谁能验证”
4. 没有懒猫 SSO 的私钥,就无法伪造任何合法 token
5. 签名通过 ≠ token 可以被使用

---

### 本章小结(一句话)

> **签名证明“这些声明只能来自这台盒子的懒猫 SSO”,
> 但不证明“你现在就该相信它们”。**



评论

0

暂无评论

说点什么呢~
收藏
0
0
0