feat(infra/domains): 域名池 + CDN 前置 + 签名端点分发 (tsk_NU9JuUweHWMt)

- 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>
This commit is contained in:
wangjia
2026-06-13 14:21:55 +08:00
parent 787151245e
commit 7d89ec9d91
32 changed files with 2572 additions and 0 deletions
+76
View File
@@ -0,0 +1,76 @@
package sign
import (
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"fmt"
"strings"
)
// GenerateKey creates a fresh Ed25519 keypair for offline use.
func GenerateKey() (ed25519.PublicKey, ed25519.PrivateKey, error) {
return ed25519.GenerateKey(rand.Reader)
}
// EncodePrivate / EncodePublic render keys as base64 (std) for storage. The
// private encoding is meant to be written to an OFFLINE medium only.
func EncodePrivate(priv ed25519.PrivateKey) string {
return base64.StdEncoding.EncodeToString(priv)
}
func EncodePublic(pub ed25519.PublicKey) string {
return base64.StdEncoding.EncodeToString(pub)
}
// DecodePrivate parses a base64-encoded Ed25519 private key.
func DecodePrivate(s string) (ed25519.PrivateKey, error) {
b, err := base64.StdEncoding.DecodeString(strings.TrimSpace(s))
if err != nil {
return nil, fmt.Errorf("sign: private key not valid base64: %w", err)
}
if len(b) != ed25519.PrivateKeySize {
return nil, fmt.Errorf("sign: private key wrong size: got %d want %d", len(b), ed25519.PrivateKeySize)
}
return ed25519.PrivateKey(b), nil
}
// DecodePublic parses a base64-encoded Ed25519 public key.
func DecodePublic(s string) (ed25519.PublicKey, error) {
b, err := base64.StdEncoding.DecodeString(strings.TrimSpace(s))
if err != nil {
return nil, fmt.Errorf("sign: public key not valid base64: %w", err)
}
if len(b) != ed25519.PublicKeySize {
return nil, fmt.Errorf("sign: public key wrong size: got %d want %d", len(b), ed25519.PublicKeySize)
}
return ed25519.PublicKey(b), nil
}
// ParseKeyRing builds a KeyRing from "keyid=base64pub" specs. Multiple specs
// (comma- or repeat-supplied) enable a rotation window where either key
// validates a document.
func ParseKeyRing(specs []string) (KeyRing, error) {
ring := KeyRing{}
for _, spec := range specs {
for _, part := range strings.Split(spec, ",") {
part = strings.TrimSpace(part)
if part == "" {
continue
}
id, b64, ok := strings.Cut(part, "=")
if !ok {
return nil, fmt.Errorf("sign: key ring spec %q must be keyid=base64pubkey", part)
}
pub, err := DecodePublic(b64)
if err != nil {
return nil, err
}
ring[strings.TrimSpace(id)] = pub
}
}
if len(ring) == 0 {
return nil, fmt.Errorf("sign: empty key ring")
}
return ring, nil
}
+213
View File
@@ -0,0 +1,213 @@
// Package sign implements the Ed25519 signing envelope used for offline
// signing of endpoint and notice distribution documents (doc/06 §3 密码学口径).
//
// Trust model:
// - The signing private key lives OFFLINE, in two physically separate
// locations. It must never enter the server, CI, or this repository.
// - The verifying public key is embedded in the client install package.
// - key_id selects which public key verifies a document; a KeyRing may hold
// more than one key so a new signing key can be rolled out before the old
// one is retired (双公钥轮换过渡).
//
// Document shape (the on-disk JSON):
//
// {
// "version": <monotonic uint64>, // anti-rollback: clients only accept larger
// "issued_at": "<RFC3339 UTC>",
// "key_id": "<key identifier>",
// "payload": { ... arbitrary JSON ... },
// "sig": "<base64(ed25519 signature)>"
// }
//
// The signature covers the canonical JSON encoding of the document WITHOUT the
// "sig" field, i.e. {version, issued_at, key_id, payload}. Because version,
// key_id and issued_at are all inside the signed bytes, an attacker cannot
// downgrade the version or swap the key without breaking the signature.
package sign
import (
"bytes"
"crypto/ed25519"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"sort"
)
// Errors returned by Verify.
var (
ErrUnknownKeyID = errors.New("sign: unknown key_id (no matching public key in ring)")
ErrBadSignature = errors.New("sign: signature verification failed")
ErrMissingSig = errors.New("sign: document has no signature")
ErrEmptyKeyID = errors.New("sign: key_id is empty")
ErrBadPayload = errors.New("sign: payload is not valid JSON")
)
// Envelope is the signed distribution document.
type Envelope struct {
Version uint64 `json:"version"`
IssuedAt string `json:"issued_at"`
KeyID string `json:"key_id"`
Payload json.RawMessage `json:"payload"`
Sig string `json:"sig,omitempty"`
}
// KeyRing maps key_id -> public key. Holding more than one entry enables a
// rotation window where documents signed by either key validate.
type KeyRing map[string]ed25519.PublicKey
// signingBytes returns the canonical bytes that are signed/verified: the
// envelope without its signature.
func (e Envelope) signingBytes() ([]byte, error) {
if e.KeyID == "" {
return nil, ErrEmptyKeyID
}
if !json.Valid(e.Payload) {
return nil, ErrBadPayload
}
unsigned := Envelope{
Version: e.Version,
IssuedAt: e.IssuedAt,
KeyID: e.KeyID,
Payload: e.Payload,
// Sig intentionally empty -> omitted by omitempty.
}
raw, err := json.Marshal(unsigned)
if err != nil {
return nil, err
}
return Canonicalize(raw)
}
// Sign computes the Ed25519 signature over e's canonical bytes and stores it in
// e.Sig (base64). The private key is supplied by the caller (loaded from the
// offline key file) and is never persisted by this package.
func Sign(priv ed25519.PrivateKey, e *Envelope) error {
if len(priv) != ed25519.PrivateKeySize {
return fmt.Errorf("sign: invalid private key size %d", len(priv))
}
msg, err := e.signingBytes()
if err != nil {
return err
}
sig := ed25519.Sign(priv, msg)
e.Sig = base64.StdEncoding.EncodeToString(sig)
return nil
}
// Verify checks e's signature against the public key selected by e.KeyID from
// ring. It returns nil only if the key is known and the signature is valid.
func Verify(e Envelope, ring KeyRing) error {
if e.Sig == "" {
return ErrMissingSig
}
pub, ok := ring[e.KeyID]
if !ok {
return ErrUnknownKeyID
}
sig, err := base64.StdEncoding.DecodeString(e.Sig)
if err != nil {
return fmt.Errorf("sign: signature is not valid base64: %w", err)
}
msg, err := e.signingBytes()
if err != nil {
return err
}
if !ed25519.Verify(pub, msg, sig) {
return ErrBadSignature
}
return nil
}
// Marshal renders the signed envelope as indented JSON suitable for publishing.
func Marshal(e Envelope) ([]byte, error) {
if e.Sig == "" {
return nil, ErrMissingSig
}
return json.MarshalIndent(e, "", " ")
}
// Parse decodes a published document into an Envelope.
func Parse(raw []byte) (Envelope, error) {
var e Envelope
if err := json.Unmarshal(raw, &e); err != nil {
return Envelope{}, fmt.Errorf("sign: cannot parse document: %w", err)
}
return e, nil
}
// Canonicalize returns a deterministic JSON encoding of raw: object keys sorted
// lexicographically, no insignificant whitespace, array order preserved. This
// guarantees signer and verifier hash identical bytes regardless of field order
// or formatting.
func Canonicalize(raw []byte) ([]byte, error) {
dec := json.NewDecoder(bytes.NewReader(raw))
dec.UseNumber() // keep integers exact; never widen to float64
var v any
if err := dec.Decode(&v); err != nil {
return nil, fmt.Errorf("sign: canonicalize decode: %w", err)
}
var buf bytes.Buffer
if err := writeCanonical(&buf, v); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func writeCanonical(buf *bytes.Buffer, v any) error {
switch t := v.(type) {
case map[string]any:
keys := make([]string, 0, len(t))
for k := range t {
keys = append(keys, k)
}
sort.Strings(keys)
buf.WriteByte('{')
for i, k := range keys {
if i > 0 {
buf.WriteByte(',')
}
kb, err := json.Marshal(k)
if err != nil {
return err
}
buf.Write(kb)
buf.WriteByte(':')
if err := writeCanonical(buf, t[k]); err != nil {
return err
}
}
buf.WriteByte('}')
case []any:
buf.WriteByte('[')
for i, e := range t {
if i > 0 {
buf.WriteByte(',')
}
if err := writeCanonical(buf, e); err != nil {
return err
}
}
buf.WriteByte(']')
case string:
b, err := json.Marshal(t)
if err != nil {
return err
}
buf.Write(b)
case json.Number:
buf.WriteString(t.String())
case bool:
if t {
buf.WriteString("true")
} else {
buf.WriteString("false")
}
case nil:
buf.WriteString("null")
default:
return fmt.Errorf("sign: unsupported JSON type %T in canonicalization", v)
}
return nil
}
@@ -0,0 +1,141 @@
package sign
import (
"crypto/ed25519"
"encoding/json"
"errors"
"testing"
)
func mustKey(t *testing.T) (ed25519.PublicKey, ed25519.PrivateKey) {
t.Helper()
pub, priv, err := GenerateKey()
if err != nil {
t.Fatalf("GenerateKey: %v", err)
}
return pub, priv
}
func TestSignVerifyRoundTrip(t *testing.T) {
pub, priv := mustKey(t)
env := Envelope{
Version: 1,
IssuedAt: "2026-06-13T00:00:00Z",
KeyID: "k1",
Payload: json.RawMessage(`{"api_domains":["a.example.com"]}`),
}
if err := Sign(priv, &env); err != nil {
t.Fatalf("Sign: %v", err)
}
if env.Sig == "" {
t.Fatal("signature not set")
}
if err := Verify(env, KeyRing{"k1": pub}); err != nil {
t.Fatalf("Verify: %v", err)
}
}
func TestVerifyRejectsTamperedPayload(t *testing.T) {
pub, priv := mustKey(t)
env := Envelope{Version: 1, IssuedAt: "t", KeyID: "k1", Payload: json.RawMessage(`{"x":1}`)}
if err := Sign(priv, &env); err != nil {
t.Fatal(err)
}
env.Payload = json.RawMessage(`{"x":2}`) // tamper after signing
if err := Verify(env, KeyRing{"k1": pub}); !errors.Is(err, ErrBadSignature) {
t.Fatalf("want ErrBadSignature, got %v", err)
}
}
func TestVerifyRejectsVersionTamper(t *testing.T) {
pub, priv := mustKey(t)
env := Envelope{Version: 5, IssuedAt: "t", KeyID: "k1", Payload: json.RawMessage(`{"x":1}`)}
if err := Sign(priv, &env); err != nil {
t.Fatal(err)
}
env.Version = 99 // attacker tries to inflate version
if err := Verify(env, KeyRing{"k1": pub}); !errors.Is(err, ErrBadSignature) {
t.Fatalf("want ErrBadSignature, got %v", err)
}
}
func TestVerifyUnknownKeyID(t *testing.T) {
pub, priv := mustKey(t)
env := Envelope{Version: 1, IssuedAt: "t", KeyID: "k1", Payload: json.RawMessage(`{}`)}
if err := Sign(priv, &env); err != nil {
t.Fatal(err)
}
if err := Verify(env, KeyRing{"other": pub}); !errors.Is(err, ErrUnknownKeyID) {
t.Fatalf("want ErrUnknownKeyID, got %v", err)
}
}
func TestKeyRotationDoublePublicKey(t *testing.T) {
oldPub, _ := mustKey(t)
newPub, newPriv := mustKey(t)
// Document signed with the NEW key, key_id "v2".
env := Envelope{Version: 1, IssuedAt: "t", KeyID: "v2", Payload: json.RawMessage(`{}`)}
if err := Sign(newPriv, &env); err != nil {
t.Fatal(err)
}
// During the rotation window the client holds BOTH public keys.
ring := KeyRing{"v1": oldPub, "v2": newPub}
if err := Verify(env, ring); err != nil {
t.Fatalf("rotation window verify failed: %v", err)
}
// A client that only has the old key must reject it.
if err := Verify(env, KeyRing{"v1": oldPub}); !errors.Is(err, ErrUnknownKeyID) {
t.Fatalf("want ErrUnknownKeyID for old-only ring, got %v", err)
}
}
func TestCanonicalizeFieldOrderInvariant(t *testing.T) {
_, priv := mustKey(t)
// Same logical payload, different key order -> identical signature.
envA := Envelope{Version: 1, IssuedAt: "t", KeyID: "k1", Payload: json.RawMessage(`{"a":1,"b":2}`)}
envB := Envelope{Version: 1, IssuedAt: "t", KeyID: "k1", Payload: json.RawMessage(`{"b":2,"a":1}`)}
if err := Sign(priv, &envA); err != nil {
t.Fatal(err)
}
if err := Sign(priv, &envB); err != nil {
t.Fatal(err)
}
if envA.Sig != envB.Sig {
t.Fatalf("canonicalization not order-invariant:\n A=%s\n B=%s", envA.Sig, envB.Sig)
}
}
func TestCanonicalizePreservesIntegers(t *testing.T) {
out, err := Canonicalize([]byte(`{"v":12345678901234567}`))
if err != nil {
t.Fatal(err)
}
if string(out) != `{"v":12345678901234567}` {
t.Fatalf("integer not preserved: %s", out)
}
}
func TestParseKeyRing(t *testing.T) {
pub, _ := mustKey(t)
ring, err := ParseKeyRing([]string{"k1=" + EncodePublic(pub)})
if err != nil {
t.Fatal(err)
}
if _, ok := ring["k1"]; !ok {
t.Fatal("k1 missing from ring")
}
if _, err := ParseKeyRing([]string{"bad"}); err == nil {
t.Fatal("want error for malformed spec")
}
}
func TestDecodePrivateRoundTrip(t *testing.T) {
_, priv := mustKey(t)
got, err := DecodePrivate(EncodePrivate(priv))
if err != nil {
t.Fatal(err)
}
if !got.Equal(priv) {
t.Fatal("private key round-trip mismatch")
}
}