// 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

[-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

[-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 .ed25519.key (private, 0600) and .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] }