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

忘机山人

发布于33天前
博客图片修整中,看不了可以先搜索公众号“忘机山人”看。

在第四章,我们已经澄清了一件非常重要的事:

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 里写得明明白白:

"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 中的任何字段,比如把:

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

改成:

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

会发生什么?

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

结果只有一个:

签名验证失败。

这保证了:

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

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


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