补充真实ACME协议回归并登记系统设置隔离任务
This commit is contained in:
parent
f3272980c0
commit
6d425259f6
87
backend/internal/certificate/lego_http_protocol_test.go
Normal file
87
backend/internal/certificate/lego_http_protocol_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
375
backend/internal/certificate/lego_protocol_fixture_test.go
Normal file
375
backend/internal/certificate/lego_protocol_fixture_test.go
Normal 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
|
||||
}
|
||||
@ -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 或整体验收。
|
||||
|
||||
@ -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。本轮接续重新读取第一台执行单元终态、下列日志与运行摘要,没有重复执行已成功的测试。第二台未操作;没有新增辅助验收脚本、备份或清空业务数据。
|
||||
|
||||
@ -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生产流程测试记录》同时间章节;历史“待回归、待部署”不代表当前状态。
|
||||
|
||||
@ -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` 已推送;第一台只运行隔离回归,不修改线上数据,不操作第二台。
|
||||
|
||||
@ -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生产流程测试记录》同时间章节。不清现有业务数据、不重复第二台主程序测试、不新增辅助脚本或远端备份。
|
||||
|
||||
@ -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 和正式签发均通过。
|
||||
|
||||
Loading…
Reference in New Issue
Block a user