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:
@@ -0,0 +1,9 @@
|
||||
package main
|
||||
|
||||
import "flag"
|
||||
|
||||
// newFlagSet returns a flag set that reports parse errors instead of exiting,
|
||||
// so the caller controls the exit path uniformly.
|
||||
func newFlagSet(name string) *flag.FlagSet {
|
||||
return flag.NewFlagSet(name, flag.ContinueOnError)
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
// Command endpoint-signer is the OFFLINE CLI that signs endpoint and notice
|
||||
// distribution documents with an Ed25519 key (doc/06 §3).
|
||||
//
|
||||
// It is meant to run on an air-gapped machine. The private key never leaves that
|
||||
// machine; only the produced *.v1.json (and the public key, embedded in the
|
||||
// client) are distributed.
|
||||
//
|
||||
// Subcommands:
|
||||
//
|
||||
// keygen -out-prefix <p> [-key-id id] generate an offline keypair
|
||||
// sign -type endpoints|notices -in payload.json -key priv.key -key-id id
|
||||
// -version N [-prev prev.json] [-issued-at RFC3339] [-out out.json]
|
||||
// verify -in doc.json -keys "id=b64[,id2=b64]" [-type ...] [-min-version N]
|
||||
// inspect -in doc.json
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/wangjia/pangolin/infra/domains/tools/internal/endpoint"
|
||||
"github.com/wangjia/pangolin/infra/domains/tools/internal/notice"
|
||||
"github.com/wangjia/pangolin/infra/domains/tools/internal/sign"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
var err error
|
||||
switch os.Args[1] {
|
||||
case "keygen":
|
||||
err = cmdKeygen(os.Args[2:])
|
||||
case "sign":
|
||||
err = cmdSign(os.Args[2:])
|
||||
case "verify":
|
||||
err = cmdVerify(os.Args[2:])
|
||||
case "inspect":
|
||||
err = cmdInspect(os.Args[2:])
|
||||
case "-h", "--help", "help":
|
||||
usage()
|
||||
return
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown subcommand %q\n", os.Args[1])
|
||||
usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Fprint(os.Stderr, `endpoint-signer — offline Ed25519 signer for endpoint/notice distribution
|
||||
|
||||
keygen -out-prefix <p> [-key-id id]
|
||||
sign -type endpoints|notices -in payload.json -key priv.key -key-id id -version N [-prev prev.json] [-issued-at RFC3339] [-out out.json]
|
||||
verify -in doc.json -keys "id=b64[,id2=b64]" [-type endpoints|notices] [-min-version N]
|
||||
inspect -in doc.json
|
||||
`)
|
||||
}
|
||||
|
||||
func cmdKeygen(args []string) error {
|
||||
fs := newFlagSet("keygen")
|
||||
outPrefix := fs.String("out-prefix", "", "output prefix; writes <prefix>.ed25519.key (private, 0600) and <prefix>.ed25519.pub")
|
||||
keyID := fs.String("key-id", "", "suggested key_id label (informational)")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if *outPrefix == "" {
|
||||
return errors.New("-out-prefix is required")
|
||||
}
|
||||
pub, priv, err := sign.GenerateKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
privPath := *outPrefix + ".ed25519.key"
|
||||
pubPath := *outPrefix + ".ed25519.pub"
|
||||
if err := os.WriteFile(privPath, []byte(sign.EncodePrivate(priv)+"\n"), 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(pubPath, []byte(sign.EncodePublic(pub)+"\n"), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("private key (0600, store OFFLINE in TWO locations): %s\n", privPath)
|
||||
fmt.Printf("public key (embed in client install package): %s\n", pubPath)
|
||||
fmt.Printf("public key (base64): %s\n", sign.EncodePublic(pub))
|
||||
if *keyID != "" {
|
||||
fmt.Printf("key ring spec: %s=%s\n", *keyID, sign.EncodePublic(pub))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cmdSign(args []string) error {
|
||||
fs := newFlagSet("sign")
|
||||
docType := fs.String("type", "endpoints", "payload type: endpoints|notices")
|
||||
in := fs.String("in", "", "input payload JSON file")
|
||||
keyFile := fs.String("key", "", "offline private key file (base64)")
|
||||
keyID := fs.String("key-id", "", "key_id stamped into the document")
|
||||
version := fs.Uint64("version", 0, "monotonic version (0 = derive from -prev + 1)")
|
||||
prev := fs.String("prev", "", "previous signed document; version defaults to its version + 1")
|
||||
issuedAt := fs.String("issued-at", "", "RFC3339 UTC timestamp (default: now)")
|
||||
out := fs.String("out", "", "output file (default: stdout)")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if *in == "" || *keyFile == "" || *keyID == "" {
|
||||
return errors.New("-in, -key and -key-id are required")
|
||||
}
|
||||
|
||||
keyBytes, err := os.ReadFile(*keyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
priv, err := sign.DecodePrivate(string(keyBytes))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ver := *version
|
||||
if ver == 0 {
|
||||
if *prev == "" {
|
||||
return errors.New("-version is required when -prev is not given")
|
||||
}
|
||||
prevRaw, err := os.ReadFile(*prev)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
prevEnv, err := sign.Parse(prevRaw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ver = prevEnv.Version + 1
|
||||
}
|
||||
|
||||
ts := *issuedAt
|
||||
if ts == "" {
|
||||
ts = time.Now().UTC().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
payloadRaw, err := os.ReadFile(*in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var env sign.Envelope
|
||||
switch *docType {
|
||||
case "endpoints":
|
||||
var p endpoint.Payload
|
||||
if err := json.Unmarshal(payloadRaw, &p); err != nil {
|
||||
return fmt.Errorf("parse endpoints payload: %w", err)
|
||||
}
|
||||
env, err = endpoint.Build(p, *keyID, ver, ts, priv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case "notices":
|
||||
var l notice.List
|
||||
if err := json.Unmarshal(payloadRaw, &l); err != nil {
|
||||
return fmt.Errorf("parse notices payload: %w", err)
|
||||
}
|
||||
if err := l.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
canon, err := json.Marshal(l)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env = sign.Envelope{Version: ver, IssuedAt: ts, KeyID: *keyID, Payload: canon}
|
||||
if err := sign.Sign(priv, &env); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unknown -type %q", *docType)
|
||||
}
|
||||
|
||||
rendered, err := sign.Marshal(env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rendered = append(rendered, '\n')
|
||||
if *out == "" {
|
||||
_, err = os.Stdout.Write(rendered)
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(*out, rendered, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "signed %s document v%d -> %s\n", *docType, ver, *out)
|
||||
return nil
|
||||
}
|
||||
|
||||
func cmdVerify(args []string) error {
|
||||
fs := newFlagSet("verify")
|
||||
in := fs.String("in", "", "signed document file")
|
||||
keys := fs.String("keys", "", "key ring: id=base64pub[,id2=base64pub] (multiple = rotation window)")
|
||||
docType := fs.String("type", "endpoints", "payload type for schema validation: endpoints|notices")
|
||||
minVersion := fs.Uint64("min-version", 0, "reject documents with version <= this (anti-rollback)")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if *in == "" || *keys == "" {
|
||||
return errors.New("-in and -keys are required")
|
||||
}
|
||||
ring, err := sign.ParseKeyRing([]string{*keys})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
raw, err := os.ReadFile(*in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch *docType {
|
||||
case "endpoints":
|
||||
env, p, err := endpoint.VerifyDocument(raw, ring, *minVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("OK: endpoints v%d key_id=%s api_domains=%d mirror_urls=%d\n",
|
||||
env.Version, env.KeyID, len(p.APIDomains), len(p.MirrorURLs))
|
||||
case "notices":
|
||||
env, err := sign.Parse(raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := sign.Verify(env, ring); err != nil {
|
||||
return err
|
||||
}
|
||||
if env.Version <= *minVersion {
|
||||
return endpoint.ErrRollback
|
||||
}
|
||||
var l notice.List
|
||||
if err := json.Unmarshal(env.Payload, &l); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := l.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("OK: notices v%d key_id=%s notices=%d\n", env.Version, env.KeyID, len(l.Notices))
|
||||
default:
|
||||
return fmt.Errorf("unknown -type %q", *docType)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cmdInspect(args []string) error {
|
||||
fs := newFlagSet("inspect")
|
||||
in := fs.String("in", "", "signed document file")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if *in == "" {
|
||||
return errors.New("-in is required")
|
||||
}
|
||||
raw, err := os.ReadFile(*in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := sign.Parse(raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("version: %d\n", env.Version)
|
||||
fmt.Printf("issued_at: %s\n", env.IssuedAt)
|
||||
fmt.Printf("key_id: %s\n", env.KeyID)
|
||||
fmt.Printf("sig: %s...\n", truncate(env.Sig, 16))
|
||||
fmt.Printf("payload: %s\n", string(env.Payload))
|
||||
return nil
|
||||
}
|
||||
|
||||
func truncate(s string, n int) string {
|
||||
if len(s) <= n {
|
||||
return s
|
||||
}
|
||||
return s[:n]
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
// Command publish-mirrors pushes a signed distribution document to ≥3 mirrors
|
||||
// and verifies every mirror serves byte-identical content (doc/05 §1/§5).
|
||||
//
|
||||
// Subcommands:
|
||||
//
|
||||
// publish -in doc.json -config mirrors.json [-name endpoints.v1.json] [-verify-only]
|
||||
// fetch -config mirrors.json -name endpoints.v1.json [-keys "id=b64"] [-type endpoints|notices]
|
||||
//
|
||||
// The mirrors config lists each destination's local content root (dir) and an
|
||||
// optional fetch_url used for read-back verification / failover fetch. The
|
||||
// per-mirror sync_cmd (e.g. wrangler / aws s3 cp) is recorded for the operator
|
||||
// but never executed here — publishing writes the object into dir, and the
|
||||
// deploy ships dir to its platform out of band.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/wangjia/pangolin/infra/domains/tools/internal/endpoint"
|
||||
"github.com/wangjia/pangolin/infra/domains/tools/internal/mirror"
|
||||
"github.com/wangjia/pangolin/infra/domains/tools/internal/sign"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
var err error
|
||||
switch os.Args[1] {
|
||||
case "publish":
|
||||
err = cmdPublish(os.Args[2:])
|
||||
case "fetch":
|
||||
err = cmdFetch(os.Args[2:])
|
||||
case "-h", "--help", "help":
|
||||
usage()
|
||||
return
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown subcommand %q\n", os.Args[1])
|
||||
usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Fprint(os.Stderr, `publish-mirrors — push signed docs to ≥3 mirrors and verify consistency
|
||||
|
||||
publish -in doc.json -config mirrors.json [-name endpoints.v1.json] [-verify-only]
|
||||
fetch -config mirrors.json -name endpoints.v1.json [-keys "id=b64"] [-type endpoints|notices]
|
||||
`)
|
||||
}
|
||||
|
||||
func loadConfig(path string) (mirror.Config, error) {
|
||||
var cfg mirror.Config
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return cfg, err
|
||||
}
|
||||
if err := json.Unmarshal(raw, &cfg); err != nil {
|
||||
return cfg, fmt.Errorf("parse mirrors config: %w", err)
|
||||
}
|
||||
if len(cfg.Mirrors) < 3 {
|
||||
return cfg, fmt.Errorf("at least 3 mirrors required, got %d", len(cfg.Mirrors))
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func cmdPublish(args []string) error {
|
||||
fs := flag.NewFlagSet("publish", flag.ContinueOnError)
|
||||
in := fs.String("in", "", "signed document to publish")
|
||||
cfgPath := fs.String("config", "", "mirrors config JSON")
|
||||
name := fs.String("name", "", "published object name (default: basename of -in)")
|
||||
verifyOnly := fs.Bool("verify-only", false, "skip writing; only verify existing mirror content matches -in")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if *in == "" || *cfgPath == "" {
|
||||
return errors.New("-in and -config are required")
|
||||
}
|
||||
cfg, err := loadConfig(*cfgPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content, err := os.ReadFile(*in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
objectName := *name
|
||||
if objectName == "" {
|
||||
objectName = filepath.Base(*in)
|
||||
}
|
||||
want := mirror.SHA256Hex(content)
|
||||
|
||||
if !*verifyOnly {
|
||||
results, perr := mirror.Publish(content, objectName, cfg.Mirrors)
|
||||
for _, r := range results {
|
||||
if r.Err != nil {
|
||||
fmt.Printf(" ✗ %-16s %v\n", r.Name, r.Err)
|
||||
} else {
|
||||
fmt.Printf(" ✓ %-16s %s sha256=%s\n", r.Name, r.Path, r.SHA256)
|
||||
}
|
||||
}
|
||||
if perr != nil {
|
||||
return fmt.Errorf("publish had failures: %w", perr)
|
||||
}
|
||||
}
|
||||
|
||||
if err := mirror.VerifyConsistency(objectName, cfg.Mirrors, want); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("consistency OK: %d/%d mirrors serve sha256=%s\n", len(cfg.Mirrors), len(cfg.Mirrors), want)
|
||||
return nil
|
||||
}
|
||||
|
||||
func cmdFetch(args []string) error {
|
||||
fs := flag.NewFlagSet("fetch", flag.ContinueOnError)
|
||||
cfgPath := fs.String("config", "", "mirrors config JSON")
|
||||
name := fs.String("name", "", "object name to fetch")
|
||||
keys := fs.String("keys", "", "optional key ring id=base64pub[,...] to verify signature on fetch")
|
||||
docType := fs.String("type", "endpoints", "payload type when -keys given: endpoints|notices")
|
||||
out := fs.String("out", "", "write fetched content here (default: stdout)")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if *cfgPath == "" || *name == "" {
|
||||
return errors.New("-config and -name are required")
|
||||
}
|
||||
cfg, err := loadConfig(*cfgPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var validate func([]byte) error
|
||||
if *keys != "" {
|
||||
ring, err := sign.ParseKeyRing([]string{*keys})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
validate = func(b []byte) error {
|
||||
if *docType == "endpoints" {
|
||||
_, _, e := endpoint.VerifyDocument(b, ring, 0)
|
||||
return e
|
||||
}
|
||||
env, e := sign.Parse(b)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
return sign.Verify(env, ring)
|
||||
}
|
||||
}
|
||||
|
||||
content, winner, err := mirror.Fetch(*name, cfg.Mirrors, validate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "fetched from mirror %q\n", winner)
|
||||
if *out == "" {
|
||||
_, err = os.Stdout.Write(content)
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(*out, content, 0o644)
|
||||
}
|
||||
Reference in New Issue
Block a user