88 lines
3.2 KiB
Go
88 lines
3.2 KiB
Go
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)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|