补充真实ACME协议回归并登记系统设置隔离任务

This commit is contained in:
xuans 2026-10-04 03:27:33 +08:00
parent f3272980c0
commit 6d425259f6
8 changed files with 487 additions and 5 deletions

View File

@ -0,0 +1,87 @@
package certificate
import (
"context"
"crypto/x509"
"net/http"
"slices"
"testing"
"time"
"git.pcux.cn/xuans/Scert/backend/internal/domain"
"git.pcux.cn/xuans/Scert/backend/internal/httpchallenge"
)
// 使用真实 lego 客户端、签名请求、挑战服务和动态 CSR 证书,不替换签发器。
// 隔离 CA 只验证本产品用到的协议流程,不代表公共 CA 或公网签发验收。
func TestLegoHTTP01Protocol(t *testing.T) {
for _, item := range []struct {
name string
domains []string
environment string
reject bool
cancel bool
}{
{name: "single", domains: []string{"example.com"}, environment: "production"},
{name: "san", domains: []string{"example.com", "www.example.com"}, environment: "staging"},
{name: "rejected", domains: []string{"example.com", "www.example.com"}, reject: true},
{name: "canceled", domains: []string{"example.com"}, cancel: true},
} {
t.Run(item.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
store := httpchallenge.NewStore()
lease, err := store.NewLease(ctx, item.domains)
if err != nil {
t.Fatal(err)
}
defer lease.Close()
ca := newProtocolCA(t, store, item.domains)
ca.reject = item.reject
if item.cancel {
ca.cancel = cancel
}
issuer := &LegoIssuer{User: ca.user, Provider: lease, HTTPClient: ca.client}
resource, err := issuer.Obtain(ctx, ObtainRequest{
Domains: item.domains, ChallengeType: domain.ChallengeHTTP01, Environment: item.environment,
})
if item.reject || item.cancel {
if err == nil || len(resource.Certificate) != 0 || len(resource.PrivateKey) != 0 {
t.Fatal("失败或取消的 ACME 订单返回了成功证书")
}
} else {
if err != nil {
t.Fatal(err)
}
if _, err := ValidateResourceForDomains(resource, item.domains); err != nil {
t.Fatal("实际 CSR 证书或密钥不匹配:", err)
}
roots := x509.NewCertPool()
roots.AddCert(ca.root)
validator := LocalValidator{Roots: roots, VerifyChain: true}
if _, err := validator.ValidatePEM(ctx, resource.Certificate, item.domains); err != nil {
t.Fatal("隔离 CA 返回的证书链校验失败:", err)
}
}
ca.mu.Lock()
defer ca.mu.Unlock()
if !ca.registered || ca.proofs == 0 || len(ca.failures) != 0 {
t.Fatalf("协议没有按预期完成:注册=%t,挑战=%d,异常=%v", ca.registered, ca.proofs, ca.failures)
}
if item.reject || item.cancel {
if ca.finalized || ca.downloads != 0 {
t.Fatal("失败订单不应提交 CSR 或下载证书")
}
} else if !ca.finalized || ca.downloads != 1 || ca.proofs != len(item.domains) || !slices.Equal(ca.csrNames, item.domains) {
t.Fatal("成功订单缺少逐域名授权、CSR 或证书下载")
}
// 在调用 Lease.Close 之前核对,避免测试清理掩盖 lego 未回收挑战。
for i, name := range item.domains {
code, _, err := ca.readProof(context.Background(), name, protocolToken(i))
if err != nil || code != http.StatusNotFound {
t.Fatalf("订单结束后挑战仍可读:域名=%s,状态=%d,错误=%v", name, code, err)
}
}
})
}
}

View File

@ -0,0 +1,375 @@
package certificate
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"math/big"
"net/http"
"net/http/httptest"
"net/url"
"slices"
"strconv"
"strings"
"sync"
"testing"
"time"
"git.pcux.cn/xuans/Scert/backend/internal/httpchallenge"
"github.com/go-jose/go-jose/v4"
)
type protocolTransport struct {
base http.RoundTripper
target *url.URL
}
func (p protocolTransport) RoundTrip(r *http.Request) (*http.Response, error) {
if r.URL.Scheme == "https" && r.URL.Path == "/directory" &&
(r.URL.Host == "acme-v02.api.letsencrypt.org" || r.URL.Host == "acme-staging-v02.api.letsencrypt.org") {
r = r.Clone(r.Context())
target := *p.target
target.Path = "/directory"
r.URL = &target
r.Host = target.Host
}
if r.URL.Scheme != p.target.Scheme || r.URL.Host != p.target.Host {
return nil, fmt.Errorf("隔离协议测试禁止访问外部地址")
}
return p.base.RoundTrip(r)
}
type protocolCA struct {
mu sync.Mutex
server *httptest.Server
challenge *httptest.Server
client *http.Client
user *AccountUser
root *x509.Certificate
key *ecdsa.PrivateKey
names []string
statuses []string
nonces map[string]bool
sequence int
registered bool
finalized bool
downloads int
proofs int
csrNames []string
chain []byte
reject bool
cancel context.CancelFunc
failures []string
}
func newProtocolCA(t *testing.T, store *httpchallenge.Store, names []string) *protocolCA {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
accountKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
template := &x509.Certificate{
SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "Scert protocol test CA"},
NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour),
IsCA: true, BasicConstraintsValid: true, KeyUsage: x509.KeyUsageCertSign,
}
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
if err != nil {
t.Fatal(err)
}
root, err := x509.ParseCertificate(der)
if err != nil {
t.Fatal(err)
}
c := &protocolCA{
key: key, root: root, names: slices.Clone(names), nonces: make(map[string]bool),
user: &AccountUser{Email: "acme@example.com", PrivateKey: accountKey},
statuses: make([]string, len(names)),
}
for i := range c.statuses {
c.statuses[i] = "pending"
}
c.challenge = httptest.NewServer(store)
t.Cleanup(c.challenge.Close)
c.server = httptest.NewTLSServer(http.HandlerFunc(c.serveHTTP))
t.Cleanup(c.server.Close)
target, err := url.Parse(c.server.URL)
if err != nil {
t.Fatal(err)
}
// 仅注入测试 CA 信任,不跳过 TLS;真实签发器的生产/测试目录选择不变。
c.client = c.server.Client()
c.client.Timeout = 5 * time.Second
c.client.Transport = protocolTransport{base: c.client.Transport, target: target}
return c
}
func protocolToken(index int) string {
return base64.RawURLEncoding.EncodeToString([]byte(fmt.Sprintf("scert-isolated-acme-token-%d", index)))
}
func (c *protocolCA) fail(w http.ResponseWriter, err error) {
c.failures = append(c.failures, err.Error())
http.Error(w, "隔离 CA 协议断言失败", http.StatusBadRequest)
}
func (c *protocolCA) signedPayload(r *http.Request) ([]byte, error) {
if r.Method != http.MethodPost || r.Header.Get("Content-Type") != "application/jose+json" {
return nil, fmt.Errorf("请求方法或签名媒体类型不正确")
}
data, err := io.ReadAll(io.LimitReader(r.Body, 64*1024))
if err != nil {
return nil, err
}
jws, err := jose.ParseSigned(string(data), []jose.SignatureAlgorithm{jose.ES256})
if err != nil || len(jws.Signatures) != 1 {
return nil, fmt.Errorf("请求缺少唯一有效签名")
}
header := jws.Signatures[0].Protected
if !c.nonces[header.Nonce] || header.ExtraHeaders[jose.HeaderKey("url")] != c.server.URL+r.URL.Path {
return nil, fmt.Errorf("签名地址或一次性随机数无效")
}
delete(c.nonces, header.Nonce)
if r.URL.Path == "/account" {
if header.JSONWebKey == nil || header.KeyID != "" {
return nil, fmt.Errorf("注册请求缺少账户公钥")
}
} else if !c.registered || header.KeyID != c.server.URL+"/account/1" || header.JSONWebKey != nil {
return nil, fmt.Errorf("请求未引用已注册账户")
}
return jws.Verify(c.user.PrivateKey.Public())
}
func (c *protocolCA) order() map[string]any {
status := "ready"
ids := make([]map[string]string, len(c.names))
auths := make([]string, len(c.names))
for i, name := range c.names {
ids[i] = map[string]string{"type": "dns", "value": name}
auths[i] = fmt.Sprintf("%s/auth/%d", c.server.URL, i)
if c.statuses[i] != "valid" {
status = "pending"
}
}
if c.finalized {
status = "valid"
}
return map[string]any{"status": status, "identifiers": ids, "authorizations": auths,
"finalize": c.server.URL + "/finalize", "certificate": c.server.URL + "/certificate"}
}
func (c *protocolCA) authorization(index int) map[string]any {
challenge := map[string]any{
"type": "http-01", "url": fmt.Sprintf("%s/challenge/%d", c.server.URL, index),
"status": c.statuses[index], "token": protocolToken(index),
}
if c.statuses[index] == "invalid" {
challenge["error"] = map[string]any{"type": "urn:ietf:params:acme:error:unauthorized", "detail": "隔离 CA 拒绝授权", "status": 403}
}
return map[string]any{
"status": c.statuses[index], "identifier": map[string]string{"type": "dns", "value": c.names[index]},
"challenges": []any{map[string]string{"type": "dns-01", "url": c.server.URL + "/forbidden-dns", "status": "pending", "token": protocolToken(index)}, challenge},
}
}
func (c *protocolCA) readProof(ctx context.Context, name, token string) (int, string, error) {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.challenge.URL+httpchallenge.PathPrefix+token, nil)
if err != nil {
return 0, "", err
}
req.Host = name
resp, err := c.challenge.Client().Do(req)
if err != nil {
return 0, "", err
}
defer resp.Body.Close()
data, err := io.ReadAll(io.LimitReader(resp.Body, 4096))
return resp.StatusCode, string(data), err
}
func (c *protocolCA) serveHTTP(w http.ResponseWriter, r *http.Request) {
c.mu.Lock()
defer c.mu.Unlock()
c.sequence++
nonce := base64.RawURLEncoding.EncodeToString([]byte(fmt.Sprintf("scert-isolated-nonce-%d", c.sequence)))
c.nonces[nonce] = true
w.Header().Set("Replay-Nonce", nonce)
w.Header().Set("Content-Type", "application/json")
write := func(value any) { _ = json.NewEncoder(w).Encode(value) }
if r.URL.Path == "/directory" && r.Method == http.MethodGet {
write(map[string]any{"newNonce": c.server.URL + "/nonce", "newAccount": c.server.URL + "/account", "newOrder": c.server.URL + "/order", "revokeCert": c.server.URL + "/revoke", "keyChange": c.server.URL + "/key-change"})
return
}
if r.URL.Path == "/nonce" && r.Method == http.MethodHead {
w.WriteHeader(http.StatusNoContent)
return
}
payload, err := c.signedPayload(r)
if err != nil {
c.fail(w, err)
return
}
switch r.URL.Path {
case "/account":
var account struct {
Terms bool `json:"termsOfServiceAgreed"`
Contact []string `json:"contact"`
}
if json.Unmarshal(payload, &account) != nil || !account.Terms || !slices.Equal(account.Contact, []string{"mailto:" + c.user.Email}) {
c.fail(w, fmt.Errorf("注册未确认条款或账户邮箱不符"))
return
}
c.registered = true
w.Header().Set("Location", c.server.URL+"/account/1")
w.WriteHeader(http.StatusCreated)
write(map[string]any{"status": "valid", "contact": account.Contact, "orders": c.server.URL + "/orders"})
case "/order":
var order struct {
Identifiers []struct {
Type string `json:"type"`
Value string `json:"value"`
} `json:"identifiers"`
}
if json.Unmarshal(payload, &order) != nil || len(order.Identifiers) != len(c.names) {
c.fail(w, fmt.Errorf("订单域名数量不符"))
return
}
for i, id := range order.Identifiers {
if id.Type != "dns" || id.Value != c.names[i] {
c.fail(w, fmt.Errorf("订单域名被修改"))
return
}
}
w.Header().Set("Location", c.server.URL+"/order/1")
w.WriteHeader(http.StatusCreated)
write(c.order())
case "/order/1":
if len(payload) != 0 {
c.fail(w, fmt.Errorf("订单查询不是签名空载荷"))
return
}
write(c.order())
case "/finalize":
if err := c.finalize(payload); err != nil {
c.fail(w, err)
return
}
write(c.order())
case "/certificate":
if !c.finalized || len(payload) != 0 {
c.fail(w, fmt.Errorf("证书下载早于订单完成或载荷非空"))
return
}
c.downloads++
w.Header().Set("Content-Type", "application/pem-certificate-chain")
_, _ = w.Write(c.chain)
default:
parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/"), "/")
if len(parts) != 2 || (parts[0] != "auth" && parts[0] != "challenge") {
c.fail(w, fmt.Errorf("客户端访问了未授权协议路径:%s", r.URL.Path))
return
}
index, err := strconv.Atoi(parts[1])
if err != nil || index < 0 || index >= len(c.names) {
c.fail(w, fmt.Errorf("授权索引无效"))
return
}
if parts[0] == "auth" {
if len(payload) != 0 {
var state struct {
Status string `json:"status"`
}
if json.Unmarshal(payload, &state) != nil || state.Status != "deactivated" {
c.fail(w, fmt.Errorf("授权更新不是合法停用"))
return
}
c.statuses[index] = "deactivated"
}
write(c.authorization(index))
return
}
if err := c.verifyChallenge(r.Context(), index); err != nil {
c.fail(w, err)
return
}
write(c.authorization(index)["challenges"].([]any)[1])
}
}
func (c *protocolCA) verifyChallenge(ctx context.Context, index int) error {
key := jose.JSONWebKey{Key: c.user.PrivateKey.Public()}
thumbprint, err := key.Thumbprint(crypto.SHA256)
if err != nil {
return err
}
code, value, err := c.readProof(ctx, c.names[index], protocolToken(index))
if err != nil || code != http.StatusOK || value != protocolToken(index)+"."+base64.RawURLEncoding.EncodeToString(thumbprint) {
return fmt.Errorf("真实 HTTP 挑战未返回账户公钥绑定证明:状态=%d", code)
}
c.proofs++
c.statuses[index] = "valid"
if c.reject {
c.statuses[index] = "invalid"
}
if c.cancel != nil {
c.cancel()
}
return nil
}
func (c *protocolCA) finalize(payload []byte) error {
for _, status := range c.statuses {
if status != "valid" {
return fmt.Errorf("未完成全部授权就提交 CSR")
}
}
var request struct {
CSR string `json:"csr"`
}
if err := json.Unmarshal(payload, &request); err != nil {
return err
}
der, err := base64.RawURLEncoding.DecodeString(request.CSR)
if err != nil {
return err
}
csr, err := x509.ParseCertificateRequest(der)
if err != nil {
return err
}
if err := csr.CheckSignature(); err != nil {
return err
}
if !slices.Equal(csr.DNSNames, c.names) {
return fmt.Errorf("CSR 的 SAN 与原订单不符")
}
c.csrNames = slices.Clone(csr.DNSNames)
leaf := &x509.Certificate{
SerialNumber: big.NewInt(2), Subject: csr.Subject, DNSNames: csr.DNSNames,
NotBefore: time.Now().Add(-time.Minute), NotAfter: time.Now().Add(30 * time.Minute),
KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
der, err = x509.CreateCertificate(rand.Reader, leaf, c.root, csr.PublicKey, c.key)
if err != nil {
return err
}
c.chain = pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
c.chain = append(c.chain, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: c.root.Raw})...)
c.finalized = true
return nil
}

View File

@ -31,4 +31,4 @@
第一台实际文件为 `/etc/nginx/conf.d/scert-odacm.conf` 和 `/etc/nginx/snippets/scert-http-challenge.conf`;仅接入管理域名对应 server,不表示其他托管域名已配置。Nginx reload 后最初请求曾由旧 worker 返回 301,随后重复检查已按新配置返回 404,原观察不隐藏。
正常公共 DNS 仍指第二台,尚未切换。内部定向请求、独立 Nginx 及迁移专项都不能替代真实 ACME 协议、公网签发和三库首次 Web 验收;这些仍待完成,不关闭 P1-03 或整体验收。
正常公共 DNS 仍指第二台,尚未切换。真实 lego 客户端已在隔离 CA 完成单域名、SAN、拒绝及取消的协议回归和五轮竞态,验证注册签名、账户绑定挑战、CSR 与证书链,不替换签发器。隔离 CA 不是公共 CA,测试目录的传输重定向不属于公共 DNS 证据;公网签发和三库首次 Web 仍待完成,不关闭 P1-03 或整体验收。

View File

@ -1,5 +1,14 @@
# Linux 生产流程测试记录
## 2026年10月4日 真实 lego 客户端隔离协议回归
- [✓] 新增 `lego_http_protocol_test.go`、`lego_protocol_fixture_test.go` 正式 Go 用例,调用产品真实 LegoIssuer 和真实 HTTP 挑战服务。仅通过已有 HTTPClient 注入把 ACME 目录限制在回环 TLS CA;信任测试 CA,不跳过 TLS,禁止访问外部 ACME 地址。
- [✓] 隔离 CA 校验一次性 nonce、受保护 URL、ES256 请求签名、账户标识、申请域名、每个挑战的公钥指纹绑定、CSR 签名和 SAN;按实际 CSR 动态签发临时证书,再由产品验证私钥匹配和测试根信任链。
- [✓] 单域名、SAN、CA 拒绝和挑战期间取消四场景通过;失败不提交 CSR 或下载证书。每个场景都在测试主动关闭租约之前核对挑战已返回 404。
- [✓] 第一台 `scert-http01-protocol-20261004-0325.service` 已 inactive、Result=success、ExecMainStatus=0;普通专项 0.686 秒,五轮专项竞态 4.614 秒。日志 `/var/tmp/scert-http01-nginx-20261004/lego-protocol.log`。
- [✓] `scert-http01-protocol-package-20261004-0324.service` 已 inactive、success、退出码 0;证书包全量 6.328 秒,随后 vet 通过。日志同目录 `lego-protocol-package.log`。测试使用 `9c26db0` 工作树加上述两个用例,生产实现未改。
- [!] 这不是公共 CA 签发、所有 ACME 协议异常一致性、真实公网 Nginx 与 CA 联合验收或三库首次 Web。没有操作第二台、线上数据库、DNS 或正式证书。
## 2026年10月4日03:14 HTTP 验证回归与第一台正式升级
时间按运行环境 UTC+8。本轮接续重新读取第一台执行单元终态、下列日志与运行摘要,没有重复执行已成功的测试。第二台未操作;没有新增辅助验收脚本、备份或清空业务数据。

View File

@ -7,7 +7,8 @@
- [✓] 域名验证方式、三库迁移二十六、HTTP 挑战存储与路由、正式签发及前端选择完成;实际配置快照授权补强已回归。
- [✓] 首批真实失败修复后,原生 Linux 全量、vet 和专项竞态通过;真实 Nginx 十项与三库迁移专项通过。前端 279 项及正式 Linux 构建通过。
- [✓] 第一台官方安装升级及运行摘要复核完成,80 挑战路径内部检查通过;没有重复第二台测试。
- [~] 实际 lego 客户端 ACME 协议回归继续补齐;完整 DNS 多账户、CNAME 委派及元数据权限仍有开发和审查缺口。
- [✓] 实际 lego 客户端隔离 ACME 四场景、五轮竞态、证书包全量与 vet 通过;不是公网或全部 ACME 标准验证。
- [~] 完整 DNS 多账户、CNAME 委派及元数据权限仍有开发和审查缺口;继续修复全局系统设置读取边界。
- [!] 公共 DNS 仍指第二台;公共签发、Smail 系统邮件与重置、三库首次 Web 未完成,P1-03 及总体生产门禁不关闭。
详见《Linux生产流程测试记录》同时间章节;历史“待回归、待部署”不代表当前状态。

View File

@ -8,12 +8,20 @@
- [✓] 实际签发快照授权回归通过,拒绝路径早于状态修改、租约和 DNS 凭据读取;覆盖 HTTP 转 DNS、默认 DNS、项目及域名变化、撤权和重试。
- [✓] 官方 Linux 包已含两个 Nginx 模板,前端 279 项通过,第一台升级至干净 `9c26db0`,运行摘要已复核。
- [✓] 第一台 80 挑战反代与严格 TLS 内部就绪通过;第二台未操作。
- [x] 同步主清单、开发清单、生产记录、核心规格和 HTTP 部署说明的最新边界,等待文档复审。
- [~] 核对真实 lego 客户端的 HTTP-01 ACME 协议覆盖,补充正式 Go 回归,不新增辅助验收脚本。
- [✓] 六份文档边界已核对并提交推送 `f327298`;第一次推送认证失败,后续重试成功。
- [✓] 真实 lego 客户端隔离 HTTP-01 协议四场景、五轮竞态、证书包全量与 vet 已在第一台通过;新增正式 Go 用例,无辅助脚本。
- [ ] 完整 DNS 多账户增删、禁用、轮换及归属生命周期。
- [ ] 完整 CNAME 委派生命周期及其余元数据接口权限复审。
- [!] 第一台公共域名、系统发信授权与三数据库首次 Web 门禁保持;公共域名实际签发不以内部请求替代。
### 系统设置读取隔离任务
沿用 P0-07 和本批元数据权限复审范围:系统全局设置只允许超级管理员读取,普通管理员和项目成员保留正常项目工作台;拒绝和成功响应均禁止共享缓存。先补旧代码失败用例,再修复,不缩减原有 DNS 账户可读范围。
- [~] 补后端正式路由回归:匿名、普通管理员、项目角色、超级管理员、原会话降权及缓存边界。
- [ ] 限制全局设置读取;前端按实时会话角色加载,降权时清除旧系统配置,失败不能沿用旧权限。
- [ ] 前后端回归、Linux 新包与第一台升级;完成前不把线上缺口标为关闭。
## 2026年10月4日02:46 HTTP 验证回归修复任务
沿用已确认需求,修改前检查点 `16224fa` 已推送;第一台只运行隔离回归,不修改线上数据,不操作第二台。

View File

@ -9,7 +9,8 @@
- [✓] 真实独立 Nginx 十项通过;SQLite、MySQL 8.0.46、PostgreSQL 14.24 迁移专项及 MySQL 中断重入、冲突结构拒绝通过。不是三库完整首次 Web 验收。
- [✓] 干净 `9c26db0` 官方 Linux 包构建、前端 279 项及产物摘要通过;第一台正式安装升级完成,本轮重新读取服务终态、运行文件摘要和严格 TLS 就绪均正常。
- [✓] 第一台 Nginx 80 挑战路径已接入,未知令牌返回无缓存 404,普通业务仍跳转 HTTPS;上游严格 TLS 不变,应用和数据库仅回环监听。这是内部诊断,不是公网签发证据。
- [~] 补齐真实 lego 客户端的 ACME 协议回归;继续 DNS 独立多账户生命周期、CNAME 委派和其余元数据权限复审。
- [✓] 真实 lego 客户端的隔离 ACME 协议回归四场景通过,五轮竞态、证书包全量与 vet 通过;覆盖注册、签名、逐域名 HTTP 证明、CSR、证书链及失败清理,不等于公网签发或完整 ACME 标准一致性认证。
- [~] 继续 DNS 独立多账户生命周期、CNAME 委派和其余元数据权限复审;已发现普通管理员可读系统设置全局元数据,正补访问限制和前端兼容。
- [!] 正常 DNS 仍指第二台,尚未获准切换;Smail 系统通知授权、真实邮件与重置、三数据库首次 Web、HTTP-01 公网签发及总体上线验收未完成。P1-03 不整项关闭。
证据路径与准确范围见《Linux生产流程测试记录》同时间章节。不清现有业务数据、不重复第二台主程序测试、不新增辅助脚本或远端备份。

View File

@ -35,6 +35,7 @@
2026年10月4日03:14 更新:HTTP-01 已接入正式签发、订单隔离路由、域名验证方式与迁移二十六;实际签发快照授权已补强。第一台 Linux 全量、vet、专项竞态、真实 Nginx 十项和三库迁移专项通过,干净 `9c26db0` 官方包已升级第一台并复核运行摘要。实际 lego 客户端完整 ACME 协议与公共域名签发仍待验收;system-dns 单账户列表不能代表独立多账户生命周期完成,完整账户及 CNAME 委派仍有开发缺口。保留六阶段未完成状态,内部就绪不替代公共入口、首次 Web 与全角色矩阵。
- 任务编号:P1-03;任务名称:完善 ACME 和 DNS 管理;优先级:P1。
- 协议证据补充:真实 lego 客户端在第一台隔离 CA 通过单域名、SAN、拒绝和取消四场景,五轮竞态及证书包全量、vet 通过;注册/JWS/HTTP 证明/CSR/证书链经过实际校验,未替换签发器。该证据不关闭公共签发、全部异常协议、DNS 账户或委派门禁。
- 当前状态:`[~] 进行中`。
- 六阶段:需求确认 `[x]`;代码实现 `[~]`;自动化测试 `[~]`;隔离环境测试 `[~]`;生产部署测试 `[~]`;最终验收 `[ ]`。
- 问题说明:已有 ACME、DNS 账户与 TXT 测试,不能证明 HTTP-01、CNAME 委派、全部 SAN 和正式签发均通过。