7d89ec9d91
- domains.md: 四组域名隔离登记 + 冷备池 ≥5 + 启用流程(不含身份信息) - cdn/terraform: Cloudflare 配置即代码(WAF/bot/速率限制/代理DNS/回源鉴权注入)+ 30min 重放 Runbook - server/internal/originauth: 回源鉴权中间件,非 CDN 网段或鉴权头不符一律 403,支持双值轮换 - tools/endpoint-signer: 离线 Ed25519 签名 CLI(端点 + 公告文档,单调版本防回滚,key_id 双公钥轮换) - tools/publish-mirrors: ≥3 镜像发布 + hash 一致性校验 + 故障转移取回 - CLIENT-CONTRACT.md: schema/验签/防回滚/合并/兜底链/channel 客户端契约 - 出站独立出口要求写入部署文档;私钥/token/身份信息一律不入库 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
133 lines
3.8 KiB
Go
133 lines
3.8 KiB
Go
package endpoint
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/wangjia/pangolin/infra/domains/tools/internal/notice"
|
|
"github.com/wangjia/pangolin/infra/domains/tools/internal/sign"
|
|
)
|
|
|
|
func buildSigned(t *testing.T, p Payload, keyID string, version uint64) ([]byte, sign.KeyRing) {
|
|
t.Helper()
|
|
pub, priv, err := sign.GenerateKey()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
env, err := Build(p, keyID, version, "2026-06-13T00:00:00Z", priv)
|
|
if err != nil {
|
|
t.Fatalf("Build: %v", err)
|
|
}
|
|
raw, err := sign.Marshal(env)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return raw, sign.KeyRing{keyID: pub}
|
|
}
|
|
|
|
func validPayload() Payload {
|
|
return Payload{
|
|
APIDomains: []string{"api-b.example.net", "api-a.example.com"},
|
|
MirrorURLs: []string{"https://m1.example.com/endpoints.v1.json"},
|
|
}
|
|
}
|
|
|
|
func TestBuildVerifyRoundTrip(t *testing.T) {
|
|
raw, ring := buildSigned(t, validPayload(), "k1", 3)
|
|
env, p, err := VerifyDocument(raw, ring, 0)
|
|
if err != nil {
|
|
t.Fatalf("VerifyDocument: %v", err)
|
|
}
|
|
if env.Version != 3 {
|
|
t.Fatalf("version = %d", env.Version)
|
|
}
|
|
// Normalization should have sorted the domains.
|
|
if p.APIDomains[0] != "api-a.example.com" {
|
|
t.Fatalf("domains not normalized/sorted: %v", p.APIDomains)
|
|
}
|
|
}
|
|
|
|
func TestRollbackRejected(t *testing.T) {
|
|
raw, ring := buildSigned(t, validPayload(), "k1", 5)
|
|
// Client already trusts version 5; a v5 (replay) or lower must be rejected.
|
|
if _, _, err := VerifyDocument(raw, ring, 5); !errors.Is(err, ErrRollback) {
|
|
t.Fatalf("want ErrRollback for equal version, got %v", err)
|
|
}
|
|
if _, _, err := VerifyDocument(raw, ring, 9); !errors.Is(err, ErrRollback) {
|
|
t.Fatalf("want ErrRollback for lower version, got %v", err)
|
|
}
|
|
// A newer current baseline that is actually older than doc is accepted.
|
|
if _, _, err := VerifyDocument(raw, ring, 4); err != nil {
|
|
t.Fatalf("v5 doc over current=4 should pass, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestTamperRejected(t *testing.T) {
|
|
raw, ring := buildSigned(t, validPayload(), "k1", 1)
|
|
// Flip a byte inside the JSON.
|
|
tampered := make([]byte, len(raw))
|
|
copy(tampered, raw)
|
|
for i := range tampered {
|
|
if tampered[i] == 'a' {
|
|
tampered[i] = 'b'
|
|
break
|
|
}
|
|
}
|
|
if _, _, err := VerifyDocument(tampered, ring, 0); err == nil {
|
|
t.Fatal("tampered document accepted")
|
|
}
|
|
}
|
|
|
|
func TestKeyRotationTransition(t *testing.T) {
|
|
oldPub, _, _ := sign.GenerateKey()
|
|
newPub, newPriv, _ := sign.GenerateKey()
|
|
env, err := Build(validPayload(), "v2", 2, "2026-06-13T00:00:00Z", newPriv)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, _ := sign.Marshal(env)
|
|
// Rotation window: client carries both old and new public keys.
|
|
ring := sign.KeyRing{"v1": oldPub, "v2": newPub}
|
|
if _, _, err := VerifyDocument(raw, ring, 0); err != nil {
|
|
t.Fatalf("rotation window verify failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestValidationRejectsEmptyDomains(t *testing.T) {
|
|
pub, priv, _ := sign.GenerateKey()
|
|
_ = pub
|
|
if _, err := Build(Payload{MirrorURLs: []string{"https://m/x"}}, "k1", 1, "t", priv); err == nil {
|
|
t.Fatal("want error for empty api_domains")
|
|
}
|
|
}
|
|
|
|
func TestValidationRejectsBadMirrorURL(t *testing.T) {
|
|
_, priv, _ := sign.GenerateKey()
|
|
p := Payload{APIDomains: []string{"a.example.com"}, MirrorURLs: []string{"not-a-url"}}
|
|
if _, err := Build(p, "k1", 1, "t", priv); err == nil {
|
|
t.Fatal("want error for bad mirror url")
|
|
}
|
|
}
|
|
|
|
func TestNoticeInPayloadValidated(t *testing.T) {
|
|
_, priv, _ := sign.GenerateKey()
|
|
p := validPayload()
|
|
p.Notice = ¬ice.Notice{ID: "n1", Level: "bogus", TitleZH: "x", TitleEn: "x", PublishedAt: "t"}
|
|
if _, err := Build(p, "k1", 1, "t", priv); err == nil {
|
|
t.Fatal("want error for invalid notice level")
|
|
}
|
|
}
|
|
|
|
func TestChannelPreserved(t *testing.T) {
|
|
p := validPayload()
|
|
p.Channel = "play-store"
|
|
raw, ring := buildSigned(t, p, "k1", 1)
|
|
_, got, err := VerifyDocument(raw, ring, 0)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.Channel != "play-store" {
|
|
t.Fatalf("channel lost: %q", got.Channel)
|
|
}
|
|
}
|