修复DNS签名参数复用并完成云端回归与安全复扫
Some checks are pending
Security Checks / security (push) Waiting to run

This commit is contained in:
smail 2026-09-21 21:55:02 +08:00
parent 33eab66a50
commit d6b74bcaad
5 changed files with 274 additions and 1 deletions

View File

@ -176,6 +176,14 @@ func aliDNSPercentEncode(value string) string {
}
func (provider *aliDNSChallenge) call(action string, values url.Values, target any) error {
// 每次请求使用独立副本,重试不能把上一次的签名带入新的签名计算。
parameters := make(url.Values, len(values))
for key, entries := range values {
if key != "Signature" {
parameters[key] = append([]string(nil), entries...)
}
}
values = parameters
nonce := make([]byte, 16)
if _, err := rand.Read(nonce); err != nil {
return errors.New("DNS 请求随机数生成失败")

View File

@ -0,0 +1,138 @@
package ssl
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"net/http"
"net/url"
"reflect"
"sort"
"strings"
"testing"
"time"
)
type dnsSignatureTransport struct {
t *testing.T
nonces map[string]bool
}
// 独立按字节编码,避免验证器与生产编码函数共享同一种错误。
func referenceDNSEncode(value string) string {
var result strings.Builder
for _, value := range []byte(value) {
if value >= 'a' && value <= 'z' || value >= 'A' && value <= 'Z' || value >= '0' && value <= '9' || strings.ContainsRune("-_.~", rune(value)) {
result.WriteByte(value)
} else {
fmt.Fprintf(&result, "%%%02X", value)
}
}
return result.String()
}
func referenceDNSSignature(values url.Values) string {
keys := make([]string, 0, len(values))
for key := range values {
if key != "Signature" {
keys = append(keys, key)
}
}
sort.Strings(keys)
var parts []string
for _, key := range keys {
for _, value := range values[key] {
parts = append(parts, referenceDNSEncode(key)+"="+referenceDNSEncode(value))
}
}
mac := hmac.New(sha1.New, []byte("test-secret&"))
_, _ = mac.Write([]byte("POST&%2F&" + referenceDNSEncode(strings.Join(parts, "&"))))
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}
func (transport *dnsSignatureTransport) RoundTrip(request *http.Request) (*http.Response, error) {
t := transport.t
if request.Method != http.MethodPost || request.URL.Scheme != "https" || request.URL.Host != "alidns.aliyuncs.com" || request.URL.RawQuery != "" {
t.Error("DNS 签名请求没有使用预期的地址和方法")
}
defer request.Body.Close()
body, err := io.ReadAll(request.Body)
if err != nil {
return nil, err
}
values, err := url.ParseQuery(string(body))
if err != nil {
return nil, err
}
if values.Get("Signature") != referenceDNSSignature(values) {
t.Error("DNS 请求签名不匹配,可能包含上一次的签名")
}
for key, expected := range map[string]string{
"Action": "AddDomainRecord", "Version": "2015-01-09", "Format": "JSON",
"AccessKeyId": "test-access", "SignatureMethod": "HMAC-SHA1", "SignatureVersion": "1.0",
"Value": "中文 a b+*/~=&%", "DomainName": "example.com",
} {
if values.Get(key) != expected {
t.Errorf("DNS 请求字段不符合预期:%s", key)
}
}
nonce := values.Get("SignatureNonce")
decoded, err := hex.DecodeString(nonce)
if err != nil || len(decoded) != 16 || transport.nonces[nonce] {
t.Error("DNS 请求随机数无效或被重复使用")
}
transport.nonces[nonce] = true
timestamp, err := time.Parse("2006-01-02T15:04:05Z", values.Get("Timestamp"))
if err != nil || time.Since(timestamp) > time.Minute || time.Until(timestamp) > time.Minute {
t.Error("DNS 请求时间戳无效")
}
if strings.Contains(string(body), "test-secret") || request.Header.Get("Content-Type") != "application/x-www-form-urlencoded" {
t.Error("DNS 请求泄露测试密钥或使用了错误的内容类型")
}
return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(`{"RecordId":"created"}`)), Header: make(http.Header)}, nil
}
func TestAutomaticDNSSigningPreservesReusableParameters(t *testing.T) {
for _, name := range []string{"fresh", "stale"} {
t.Run(name, func(t *testing.T) {
transport := &dnsSignatureTransport{t: t, nonces: make(map[string]bool)}
provider := &aliDNSChallenge{
accessKey: "test-access", secretKey: "test-secret", endpoint: "https://alidns.aliyuncs.com/",
client: &http.Client{Transport: transport},
}
values := url.Values{"DomainName": {"example.com"}, "Value": {"中文 a b+*/~=&%"}}
expected := url.Values{"DomainName": {"example.com"}, "Value": {"中文 a b+*/~=&%"}}
if name == "stale" {
values.Set("Signature", "stale-signature")
expected.Set("Signature", "stale-signature")
}
for i := 0; i < 3; i++ {
var result struct {
RecordID string `json:"RecordId"`
}
if err := provider.call("AddDomainRecord", values, &result); err != nil || result.RecordID != "created" {
t.Fatal("DNS 签名请求未正确返回")
}
if !reflect.DeepEqual(values, expected) {
t.Error("DNS 请求改写了调用者参数,重试会带入之前的签名")
}
}
})
}
}
func TestAutomaticDNSSignatureReferenceVector(t *testing.T) {
// 固定向量由独立 Python 标准库计算;只是测试样本,不是真实云凭据。
values := url.Values{
"Action": {"AddDomainRecord"}, "Version": {"2015-01-09"}, "Format": {"JSON"},
"AccessKeyId": {"test-access"}, "SignatureMethod": {"HMAC-SHA1"}, "SignatureVersion": {"1.0"},
"SignatureNonce": {"00000000000000000000000000000000"}, "Timestamp": {"2026-09-21T00:00:00Z"},
"DomainName": {"example.com"}, "RR": {"_acme-challenge"}, "Type": {"TXT"}, "Value": {"a b+*/~=&%"},
}
if referenceDNSSignature(values) != "s+e5Vs2kLbIFh5nFBykkdS2IrFU=" {
t.Fatal("独立签名验证器不符合固定向量")
}
}

View File

@ -0,0 +1,95 @@
# DNS 签名参数隔离补充验收
## 结论与范围
2026 年 9 月 21 日,基于已推送的 `33eab66` 继续复核 DNS 签名。
发现并修复请求函数改写调用者参数、复用参数时旧签名参与新签名计算的缺陷。
本地竞态回归及云端 SSL 包隔离回归通过,**不构成真实 DNS 续期、服务商认证、全协议换证或全项目生产验收通过。**
当前 `Present`、记录查询及 `CleanUp` 每次都新建请求参数,没有证据表明现有线上调用正在触发复用缺陷。
本批是对私有请求函数的健壮性整改,不能反推历史证书问题的根因。
## 任务状态
- [✓] 独立签名验证器及固定向量复核。
- [✓] 干净参数连续复用、已有旧签名参数两组失败反例。
- [✓] 请求参数深复制,签名前排除旧签名,不回写调用者对象。
- [✓] 请求方法、编码、随机数、时间戳、签名及密钥不明文发送的回归断言。
- [✓] Windows SSL 包竞态回归,明确保留三项平台权限跳过。
- [✓] 云端 Linux SSL 包全量测试及独立只读证据复核,零跳过。
- [!] G505 的协议迁移、风险评估与可审计排除尚未关闭。
- [!] 真实云凭据轮换、服务商 API 实测、真实续期及全协议换证尚未完成。
## 修复与反例
原请求函数直接向传入的 `url.Values` 写入公共字段和签名。
第一次请求结束后,参数对象残留 `Signature`;再次使用它时,签名原文把旧值也包含进去。
预先带有旧签名的参数会从第一次调用开始不匹配。
现在为每次请求复制参数及其切片,复制时排除 `Signature`,之后只修改独立副本。
随机数仍由密码学随机源生成,每次请求重新生成时间戳;没有改动签名算法、TLS 校验、超时或跳转策略。
原始失败日志:
- `dns-signature-before-20260921.log`:带旧签名参数的三次请求均不匹配,且调用者参数被改写。
- `dns-signature-before-v2-20260921.log`:增加干净参数复用反例,第二次起签名不匹配;两组反例均失败。
原始日志没有删除或覆盖。验证器按 UTF-8 字节独立编码,不调用生产的百分号编码函数。
固定输入由独立 Python 标准库计算,向量文件为 `dns-signature-vector-20260921.json`。
向量中的密钥、签名都是公开测试样本,不是真实云凭据;它不是服务商发布的官方测试向量。
## 协议参考与告警边界
从阿里云官方 Go SDK 仓库的 `sdk/auth/rpc_signature_composer.go` 取得参考实现,核对 RPC 签名字段、签名前移除旧签名及编码流程。
这仅证明该 SDK 实现使用相应签名方式,不证明当前 API 必须使用该算法,也不代表服务商禁止迁移其他签名协议。
本轮没有擅自把 HMAC-SHA1 替换为其他算法,亦不将其作为密码存储或证书签名算法使用。
G505 继续保留,没有添加忽略标记,也没有把“协议使用”自动认定为安全风险已消除。
参考地址:
```text
https://raw.githubusercontent.com/aliyun/alibaba-cloud-sdk-go/master/sdk/auth/rpc_signature_composer.go
```
下载快照为 `alidns-sdk-signature-20260921.go`,SHA-256 为 `e1b4f41a66e86f5bb6e5601c3f12fb7375f8a2b19df988bb6a3754cd81c3f333`。
初次访问帮助中心旧地址得到 HTTP 200,但内容实际为错误页;该页面不作为协议依据,保留快照 `alidns-signature-official-20260921.html`。
## 本地与云端结果
| 范围 | 实际结果 | 未覆盖内容 |
| --- | --- | --- |
| Windows SSL 包竞态回归 | 25 个顶层测试及 26 个子测试通过,3 个顶层测试跳过,零失败 | Windows 当前身份无法创建符号链接 |
| Linux SSL 包全量隔离回归 | 28 个顶层测试及 26 个子测试通过,零跳过、零失败 | 不是整个后端全量测试,不是真实 DNS 服务商联调 |
| 新签名专项 | 两组复用场景各连续三次验证,固定向量通过 | 随机数有限样本不等于数学上的唯一性证明 |
| 收尾复核 | 程序、日志与源码摘要匹配,六个保护服务及正式程序不变 | 正式实例没有升级 |
云端目录:`/var/tmp/smail-review-ssl-apex-20260921/20260921-210920-dns-signature`。
测试使用非特权用户、私有网络与私有临时目录,内存上限 256 MiB,CPU 配额 50%,禁用交换,禁止访问正式 `/opt/smail`。
测试中的请求由受控模拟传输层处理,没有使用真实云凭据、修改公网 DNS 或访问 ACME 服务商。
临时单元已被系统回收,状态为 `LoadState=not-found`、inactive、PID 为零;清理停止命令返回 5 表示单元已不存在,不冒充该命令返回零。
测试程序 SHA-256:`4b374cd7a8f0e4516336a868a9797db7d7803feabbfc8b055ce229da70b7e1bd`。
云端日志 SHA-256:`0fb5619ab93cd6c23b804eb267845489d2dfe5eafd8ae5cd3ac736c24e6bd06b`。
21:20 完成独立只读复核,本包源码与编译前记录一致,正式六服务与程序保持原状。
证据位于 `test-log/production-remediation-20260921/`:
- `dns-signature-after-race-20260921.log`
- `dns-signature-sources-20260921.json`
- `dns-signature-build-20260921.log`
- `ssl-apex-dns-signature/test.log`
- `ssl-apex-dns-signature/result.json`
- `dns-signature-independent-recheck-20260921.json`
## 当前候选完整静态复扫
包含本批 DNS 修复及上一批发布回滚修复的后端已完成 Linux 目标全项目扫描:84 个包、348 个非测试源码文件,无包加载错误,38 条告警,退出 1。
规则分布为 G115 两条、G202 五条、G304 二十条、G302 八条、G306 两条、G505 一条。
忽略标记仍为原有的五个;告警规则、文件和归一化代码片段与此前报告相同,没有通过屏蔽规则降低数量。
619 份后端 Go 源码摘要未漂移。详见 `静态安全告警复核记录.md` 和 `gosec-release-dns-final-linux-20260921.json`。
本次扫描完成不等于扫描通过,更不等于生产放行。
## 后续门禁
仍需补齐签名协议的当前服务商支持范围与迁移评估、历史云凭据处置、真实续期及全协议换证。
浏览器、外部投递、迁移中断临界点、长时容量、异机灾备与最终统一候选验收也不能由本批替代。

View File

@ -31,7 +31,8 @@
- [✓] 管理员恢复整数边界及系统程序路径整改,云端 Linux 包 41 个顶层测试零跳过通过;新候选三库空数据实机复验通过。
- [✓] 修复自动 DNS 证书加载与导出遗漏根域名校验,保留整改前云端失败反例;新候选的证书专项与三库回归通过。
- [✓] SQLite 同引擎及双 SQL 同引擎、跨引擎切库后的真实进程重启与目标不可用恢复;分阶段证据见后文。
- [ ] 涉及 SQLite 的跨引擎应用级重启矩阵、SQL 中断及 SQLite 提交/配置切换临界点恢复。
- [✓] 涉及 SQLite 的四个跨引擎应用级方向分阶段通过;不同候选与资源配置的证据不合并为最终候选统一矩阵。
- [ ] SQL 中断及 SQLite 提交/配置切换临界点恢复。
- [ ] 最新候选浏览器初始化、账号恢复及完整用户操作闭环。
- [!] Windows 提权本地通道验收需要管理员会话,当前终端未提权。
- [!] 生产 DNS 自动续期需要安全注入有效且已轮换的最小权限凭据,当前为手动托管证书。
@ -45,6 +46,14 @@
Windows 发布包回归与竞态检查分别为 21 个测试及子测试通过,Linux 云端隔离专项 23 项通过,均零跳过。正式六服务及程序未变,没有执行正式升级。
发布包 Linux 目标静态复扫仍有 4 条 G304 和 1 条 G306,退出 1,不能排除路径竞争、父目录信任、并发升级和崩溃一致性风险。整体生产验收仍未通过。
## DNS 签名参数隔离补充
详见 `DNS签名参数隔离补充验收.md`。基于 `33eab66` 复现并修复请求参数复用时旧签名参与新签名计算的问题,原始失败保留。现有调用链每次新建参数,因此不能据此认定线上续期故障根因。
Windows SSL 包竞态回归 51 个测试及子测试通过、3 个符号链接权限跳过;云端 Linux SSL 包全量隔离测试 54 项通过、零跳过。独立复核源码、程序及日志摘要一致,正式六服务及程序未变。
本轮采用受控模拟传输,不使用真实云凭据、不修改公网 DNS;G505 风险、真实续期及全协议换证仍未关闭。
两批修复之后完成当前后端全项目 Linux 目标静态复扫:84 个包、348 个非测试源码文件,无包加载错误,仍有 38 条告警,退出 1。告警集合和原有五个忽略标记均未变化;619 份后端源码摘要未漂移。完整证据见 `静态安全告警复核记录.md`,整体生产门禁不放行。
## 操作边界
- 仅操作 Smail;不停止共享数据库、Nginx 或其他项目。

View File

@ -1,5 +1,28 @@
# 静态安全告警复核记录
## 发布与 DNS 修复后的全项目复扫
2026 年 9 月 21 日,对包含发布回滚与 DNS 参数隔离修复的当前后端执行完整 Linux 目标扫描:`GOOS=linux`、`GOARCH=amd64`、`CGO_ENABLED=0`,扫描目录为 `./...`,并发度为 1。
实际检查 84 个包、348 个非测试源码文件,共 56999 行;无包加载错误,工具退出 1,报告 38 条告警。
| 规则 | 数量 |
| --- | --- |
| G115 | 2 |
| G202 | 5 |
| G304 | 20 |
| G302 | 8 |
| G306 | 2 |
| G505 | 1 |
原始报告为 `test-log/production-remediation-20260921/gosec-release-dns-final-linux-20260921.json`,退出记录为 `gosec-release-dns-final-exit-20260921.json`。
按规则、文件及去除行号后的代码片段与管理员恢复批次的报告比较,告警集合一致,不只是总数相同。
报告中的忽略标记数量仍为原有的 5,没有新增批量忽略;不能把 38 条告警称为扫描通过。
619 份当前后端 Go 文件摘要在扫描期间未漂移,独立比对结果为 `release-dns-final-recheck-20260921.json`。
原有 7 条专项复核证据的范围保持不变,其他 31 条尚未整体关闭。
新增回滚测试和签名参数复用测试不构成 G304、G306 或 G505 的全量排除。
整体生产安全门禁继续未通过。
## 发布安装包局部复扫补充
2026 年 9 月 21 日,发布安装回滚整改后,仅对 `internal/release` 执行 Linux 目标复扫。