SAML 联邦、元数据与证书轮换:把一次登录变成可验证的信任链
企业应用接入 SAML 后,最危险的故障往往不是页面明确报错,而是信任链悄悄分叉:测试租户可以登录,生产租户却把断言发往旧 ACS;管理员在身份平台替换了一张“证书”,所有用户随即失去入口;响应签名显示有效,应用却从另一个未签名节点读取了管理员组。控制台里的“已启用”只能说明配置对象存在,不能证明浏览器带回来的消息属于目标 IdP、目标 SP、当前请求和当前时间窗。
排查必须回到协议对象。SAML 2.0 Web Browser SSO 不是一张证书加一个回调地址,而是 IdP 与 SP 通过受控元数据交换实体标识、端点、Binding 和公钥,再让 AuthnRequest、Response、Assertion、本地会话沿同一条关联链运行。只要能保存请求 ID、响应 ID、签名覆盖节点、Audience、Recipient 和证书序列号,登录成功与失败就都能被解释;缺少这些证据时,放宽时钟、关闭签名或临时接受任意属性只会把故障变成漏洞。
先认清联邦中的对象和所有者
SAML 联邦至少有两个行政主体。身份提供方 IdP 认证人员,并按策略发布 NameID、组或其他属性;服务提供方 SP 接收断言、映射本地账号并建立应用会话。两者共享的是协议契约,不共享数据库。entityID 是实体的稳定标识,不是展示名称;SingleSignOnService 是 IdP 接收认证请求的端点;AssertionConsumerService(ACS)是 SP 接收 SAMLResponse 的端点。一个 SP 可以有多个 ACS,每个端点还带 Binding、index 和默认标记,因此“域名相同”并不能证明端点选对。
IdP metadata 以 IDPSSODescriptor 描述登录服务、NameID 格式和可用公钥,SP metadata 以 SPSSODescriptor 描述 ACS、可选登出服务、公钥及是否要求请求签名。SAML 没有与 OIDC Discovery 等价的公共统一发现地址;元数据 URL、静态文件或联邦聚合器必须通过组织认可的渠道登记。规范对象可从 OASIS SAML 2.0 技术概览 和 SAML 2.0 Metadata 规范 交叉确认。
一次 SP 发起登录时,SP 生成唯一 AuthnRequest ID,并把目标 IdP、ACS、RelayState、本地浏览器会话和失效时间绑定起来。IdP 校验 SP 与请求后签发 Response,其中通常包含 Assertion、Subject、Conditions、AuthnStatement 和属性。SP 必须验证后再建立自己的会话;SAML 断言不是应用 Cookie,删除断言缓存也不会自动删除已经建立的应用会话。
元数据是可缓存的信任配置
收到 metadata XML 后,第一步只是确认它是可解析文档;真正导入还要确认签名锚、期望 entityID、实体角色、端点协议与主机、Binding、有效期和 KeyDescriptor 用途。HTTPS 保护下载过程中的传输机密性与服务器认证,但 URL 指向错误租户、上游被错误发布或文件在离线交付中被替换时,HTTP 连接成功仍不能证明元数据属于目标伙伴。对签名 metadata,应使用预先经独立渠道取得的信任锚验证,不能从待验证 XML 中临时导出证书再宣布可信。
下面的命令可在测试联邦中复制执行。TRUST_PEM 保存的是 metadata 签名信任锚的公钥证书,EXPECTED_ENTITY_ID 是接入登记值;两者都不从下载结果推导。
set -euo pipefail
export SAML_METADATA_URL='https://idp.example.test/metadata'
export TRUST_PEM='/secure/path/metadata-signing-trust.pem'
export EXPECTED_ENTITY_ID='https://idp.example.test/entity'
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
curl --fail --silent --show-error --location \
--proto '=https' --tlsv1.2 \
"$SAML_METADATA_URL" > "$tmp/idp-metadata.xml"
xmllint --noout --nonet "$tmp/idp-metadata.xml"
xmlsec1 --verify --trusted-pem "$TRUST_PEM" "$tmp/idp-metadata.xml"
actual_entity_id="$(xmllint --xpath \
'string(/*[local-name()="EntityDescriptor"]/@entityID)' \
"$tmp/idp-metadata.xml")"
test "$actual_entity_id" = "$EXPECTED_ENTITY_ID"
echo "PASS entityID=$actual_entity_id"成功输出只证明三件事:XML 可解析、签名能回到指定锚、实体标识匹配。导入器还应限制文档大小,禁用网络实体与不需要的 DTD,拒绝重复 ID 和歧义结构,并检查 metadata 的 validUntil、cacheDuration 与本地刷新策略。缓存不是永久信任副本;刷新失败时保留上一份仍在有效期内的可信版本可以维持服务,过期后继续无限使用则掩盖伙伴退出和密钥撤销。
TLS、签名和加密证书各司其职
“SAML 证书”这个说法会把三条完全不同的密钥链混在一起。HTTPS 端点的 TLS 证书由 Web 服务器持有私钥,用于建立传输通道和证明域名;SAML signing key 的私钥由消息签发方持有,用于签署 metadata、AuthnRequest、Response 或 Assertion;SAML encryption key 的私钥由解密方持有,用于解开加密 Assertion。替换负载均衡器的 TLS 证书不会自动轮换 metadata 中的 signing key,发布新的 signing certificate 也不会修复浏览器对 HTTPS 主机名的拒绝。
KeyDescriptor use="signing" 表示公钥用于验证签名,use="encryption" 表示对端可用该公钥加密;use 省略时如何解释要依协议和产品实现确认。签名提供完整性、来源认证和签发者不可抵赖证据;XML Encryption 保护断言内容在浏览器中转时的机密性,但它不证明 Audience、Recipient、时间和重放状态正确。即使 Assertion 已加密,外层 Response 与浏览器 POST 仍应走 TLS。
传统 PKIX 会围绕 CA 链、主机名、用途和吊销状态验证证书;许多 SAML 部署则把 metadata 中的证书直接固定为联邦公钥载体,不要求证书 Subject 与 HTTP 域名一致。采用哪种信任模型必须在接入记录中明确,并由导入器一致执行。无论采用哪一种,都不能信任攻击者随 SAMLResponse 携带的陌生证书,也不能因为证书还在有效期内就忽略 metadata 已删除该 key 的事实。
从租户登记到项目接入
接入先为每个环境分配不同 SP entityID 和 ACS,避免测试断言被生产入口接受。IdP 租户登记 SP metadata 后,应把用户或组只分配给最低权限测试群体;SP 侧登记预期 IdP entityID、可信 metadata 获取方式、允许的 Binding、NameID 策略和属性白名单。多租户 SaaS 不应根据浏览器提交的任意 issuer 动态建立信任,应先由租户标识定位已登记连接,再只接受该连接配置中的 IdP 和 ACS。
应用项目里,协议库负责 XML 解析、签名与条件校验,业务适配层只接收验证器返回的“已验证主体”。不要先把 XML 转成通用 Map 再由业务代码挑第一个 Assertion;这会重新引入 XML Signature Wrapping 风险。一个可审计的适配接口可以保持如下形状:
VerifiedPrincipal {
federationConnectionId
idpEntityId
spEntityId
responseId
assertionId
requestId
subjectKeyHash
signedNodeKind
signingKeyId
authnContext
allowedAttributes
}
SessionFactory.create(VerifiedPrincipal principal, RelayStateBinding binding)
ReplayStore.consumeOnce(responseId, assertionId, expiresAt)allowedAttributes 必须由属性映射白名单生成。IdP 发来 admin=true 或一个未登记的高权限组时,认证可以保持成功,但本地授权不得因此扩大。NameID 的格式、可重分配性和租户内唯一性决定它能否作为连接键;邮箱容易改名或复用,更稳妥的做法是使用 IdP 提供的不可变主体标识,并把显示邮箱当普通属性。
正向实验逐项证明断言可接受
测试 IdP 签发一条合法 Response 时,不要只观察浏览器跳回首页。协议测试应固定一套非生产测试证书和匿名化夹具,逐项断言解析器消费的是被签名覆盖的节点,且 Issuer、Destination、Recipient、AudienceRestriction、InResponseTo、NotBefore、NotOnOrAfter 与 AuthnContext 都满足本地策略。
GIVEN
受信 IdP metadata 中含 signing key K1
SP entityID = https://sp.example.test/entity
ACS = https://sp.example.test/saml/acs
已登记 AuthnRequest ID = _req-001
WHEN
IdP 用 K1 签署测试 Response/Assertion
Audience = SP entityID
Destination 与 Recipient = ACS
InResponseTo = _req-001
Response ID 与 Assertion ID 首次出现
THEN
签名覆盖节点由验证器明确返回
replay store 原子写入两个 ID
只映射白名单属性
创建最低权限测试会话
审计记录 connection、request、response、assertion 与 key ID时间校验使用有限且显式的 clock skew,服务器时间应由可靠时间源同步。偏差预算不是修复手段:若持续靠扩大 skew 才能登录,第一证据应是 SP、IdP 与代理节点的时钟偏移及断言时间字段。NotOnOrAfter 是排他上界,接近边界的并发请求必须按库的精确定义测试,不能依赖肉眼读取时间字符串。
反向实验让错误稳定暴露
第一组反测针对 metadata。复制下载文件并改动被签名的 entityID,签名验证必须失败。这个实验能证明导入器没有仅检查“存在 Signature 元素”。
cp "$tmp/idp-metadata.xml" "$tmp/idp-metadata-tampered.xml"
perl -0pi -e 's/entityID="/entityID="tampered-/' \
"$tmp/idp-metadata-tampered.xml"
if xmlsec1 --verify --trusted-pem "$TRUST_PEM" \
"$tmp/idp-metadata-tampered.xml" >/dev/null 2>&1; then
echo 'FAIL: tampered metadata accepted' >&2
exit 1
fi
echo 'PASS: tampered metadata rejected'第二组反测要由测试 IdP 合法重新签名,因为“随手改 XML 导致签名失败”不能证明语义校验存在。依次把 Audience 改成另一 SP、Recipient 改成另一 ACS、InResponseTo 改成未知 ID、生成尚未生效或已经过期的 Assertion;每次都应得到明确拒绝原因且不创建会话。把同一原始 Response 提交第二次,第一次可以成功,第二次必须被 replay store 拒绝。再加入重复 ID、双 Assertion 和 Signature Wrapping 安全夹具,验证业务层永远拿不到未被签名覆盖的 sibling 节点。
IdP 发起登录没有对应 AuthnRequest,天然缺少 InResponseTo 关联。若业务确实启用该入口,应使用独立策略约束允许的连接、目标资源、RelayState 与重放缓存,绝不能让 SP 发起入口在“找不到请求 ID”时自动降级为无关联接受。
故障证据要能定位到校验阶段
登录失败可以按阶段分型。metadata 阶段常见证据是下载状态、最终 URL、签名锚 ID、entityID、缓存版本和过期状态;请求阶段看 AuthnRequest ID、目标 IdP、ACS、Binding 和 RelayState 绑定;响应阶段看解析拒绝类别、Issuer、Destination、Recipient、Audience、时间条件、签名 key ID 与重放命中;会话阶段再看本地账号映射、属性策略和 Cookie 写入。浏览器网络面板里的 Base64 文本只适合隔离测试,完整断言常含个人信息和可重放材料,不应贴进工单或普通日志。
常见的“签名算法不支持”可能是算法策略拒绝、metadata 未刷新、key usage 不匹配或验证了错误节点;“用户不存在”可能是 NameID 格式变化、租户连接选错或属性发布策略未命中;“偶发过期”可能是代理排队、节点时钟漂移或断言窗口过窄。日志若只写 SAML validation failed,这些分支无法区分。建议记录稳定错误码,例如 metadata_signature_invalid、issuer_mismatch、audience_mismatch、request_not_found、replay_detected,再附请求关联 ID 和不可逆主体摘要。
敏感字段按用途分层保存:私钥只进入密钥托管或硬件保护设施;完整 Response、Assertion、NameID、组和人员属性不进入常规日志;证书指纹、序列号、公钥摘要、entityID、ACS、算法、缓存版本和错误码可以进入受控审计。SAML 密钥轮换任务需要访问私钥,但日常排障人员通常只需查看公钥信息和验证结果。
新旧公钥并存完成无中断轮换
轮换 signing key 时,先在 metadata 发布新公钥 K2,同时保留 K1;等待所有消费方刷新并报告已识别 K2,再让签发方切换到 K2;持续验证 K2 消息成功且 K1 仍在计划窗口内可验证;最后停止 K1 签名并从 metadata 移除 K1。若对端只支持单 key,不能假装执行双 key 轮换,应建立第二连接、维护窗口或可回退的协调步骤。
阶段 A metadata: [K1] signer: K1 consumer: K1
阶段 B metadata: [K1, K2] signer: K1 consumer: K1 + K2
阶段 C metadata: [K1, K2] signer: K2 consumer: K1 + K2
阶段 D metadata: [K2] signer: K2 consumer: K2
回退规则:
B 失败 -> 修复 metadata 分发,不切 signer
C 失败 -> 在 K1 私钥仍受控时切回 K1,保留失败证据
D 之后 -> K1 私钥销毁或归档按组织密钥政策执行metadata signing key、消息 signing key、encryption key 和 TLS key 可能由不同团队、不同设备持有,轮换工单必须点名对象。若 K1 私钥疑似泄露,常规并存窗口不再适用:先阻止继续签发,发布并传播新信任,缩短或清空相关会话,保留已见 Response/Assertion ID 以阻止重放,并审计泄露窗口内的登录。删除旧公钥只能阻止后续验证,不能自动撤销此前已建立的应用会话。
容量和成本藏在 XML、缓存与会话里
SAML 请求大多经过浏览器,Redirect Binding 会受 URL 与代理头大小限制,POST Binding 会增加表单体积;大量组属性会让 Response 膨胀,带来网关拒绝、解析 CPU 增长和日志泄露风险。授权所需属性应最小化,高基数组不应无差别塞入每次断言。XML 解析器必须限制文档大小、节点深度、属性数量和处理时间,重放缓存则按“峰值登录速率 × 最大接受窗口”估算键数量,并设置到期清理。
metadata 刷新不能在每次登录时同步下载,否则伙伴抖动会直接变成登录抖动。合理设计是后台刷新、签名与语义校验成功后原子替换可信快照,登录路径只读内存或本地持久化版本。监控应观察刷新成功率、可信快照年龄、未知 key、签名失败、Audience/Recipient 错误、重放拒绝、断言体积分位和 ACS 延迟。生产阈值来自登录 SLO、伙伴数量和基线压测,不应复制一个通用数字。
成本不仅是 IdP 许可,还包括每个租户的连接对象、证书轮换协调、属性映射审核、测试账号、审计保留和应急值守。连接数增长后,手工上传 XML 与邮件换证会成为主要风险,应使用带审批的连接配置仓库、差异预览和 canary 登录,但自动化不得把私钥或完整断言写入流水线产物。
迁移和退出以信任与会话清零收尾
更换 IdP 或 SP 时,先建立第二条联邦连接并使用独立 entityID 或明确的连接标识,让小批测试主体走新链路。比较两侧不可变主体映射、组与角色结果、认证上下文、会话时长和审计关联,再逐步迁移租户。迁移期间不要让两个 IdP 的同一邮箱自动合并成本地账号;应由受控映射把旧主体与新主体绑定到同一内部 ID,并保留冲突处理记录。
退出一条连接时,先停止新主体分配和新登录,撤销或删除应用中的现存会话,再从双方配置删除端点、公钥与属性发布策略。随后验证旧 IdP 新请求被拒绝、缓存 metadata 不再续期、旧 Response 重放失败、旧证书不再被接受,并清理 RelayState、请求关联与重放缓存到其自然失效。私钥按密钥政策销毁或受控归档,公钥和配置摘要按审计政策保留。
最后的退出证据应能回答:哪个 entityID 已停用,哪一版本 metadata 最后生效,哪些 signing/encryption key 已移除,何时停止新会话,现存会话如何核销,旧入口返回什么稳定错误。协议细节可继续查阅 SAML 2.0 Core 与 SAML 2.0 Bindings;组织自己的退出记录必须把这些协议对象落到真实租户、应用与责任人上。
