diff --git a/.gitignore b/.gitignore index 163c855..21a746e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,9 @@ coverage.out *.bak *~ *.tmp + +# Ed25519 signing private keys produced by scripts/gen-signing-key.sh — +# must never be committed. +signing-key.hex +*.signing-key.hex +*.key diff --git a/README.md b/README.md index ac2c1d9..985dc77 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ path requires upstream wiring — see the TODO on `Service.IsTrustedWithKey`. | File | What it does | |---|---| -| `data.go` | Embedded JSON list. `Load`, `All`, `IsTrusted(nodeID) → (name, ok)`, `IsTrustedWithKey(nodeID, pubKey) → (name, ok)`, `SetForTest`. | +| `data.go` | Embedded JSON list. `Load`, `All`, `IsTrusted(nodeID) → (hostname, ok)`, `IsTrustedWithKey(nodeID, pubKey) → (hostname, ok)`, `SetForTest`. | | `runtime.go` | `Run(ctx)` — periodic fetcher over HTTPS to `raw.githubusercontent.com`. | | `service.go` | `*Service` — `coreapi.Service` adapter (`Name/Order/Start/Stop` + `IsTrusted`). Build tag `!no_trustedagents`. | | `service_disabled.go` | Stub `*Service` when build tag `no_trustedagents` is set. | diff --git a/data.go b/data.go index 2ecedd4..a9f0f8e 100644 --- a/data.go +++ b/data.go @@ -8,8 +8,10 @@ // // The list is plain JSON in this directory, embedded at build time and // refreshed hourly from raw.githubusercontent.com by -// plugins/trustedagents.Run. Authenticity comes from HTTPS to GitHub -// plus repo write access — there is no separate signature check. +// plugins/trustedagents.Run. Authenticity is handled in two tiers: +// unsigned lists are accepted over TLS with a warning (backward-compatible); +// signed lists require embeddedPubKey to be configured and undergo full +// Ed25519 verification via VerifyAndStripSig before the payload is trusted. // // Adding an agent: edit trusted-agents.json, commit. Daemons in the // field pick it up within ~1h. Brand-new daemons get the embedded copy @@ -206,8 +208,8 @@ func All() []Agent { // skipped (backward-compatible). Set this to the real public key once the // signing infrastructure is in place. // -// TODO: inject via -ldflags at build time so the same binary can verify -// different lists (dev/staging/prod). +// To inject: go build -ldflags "-X github.com/pilot-protocol/trustedagents.embeddedPubKeyHex=<64-hex-chars>" +// Generate keypair: scripts/gen-signing-key.sh var embeddedPubKey = ed25519.PublicKey{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, diff --git a/scripts/gen-signing-key.sh b/scripts/gen-signing-key.sh new file mode 100755 index 0000000..84c7989 --- /dev/null +++ b/scripts/gen-signing-key.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# gen-signing-key.sh — generate an ed25519 keypair for trusted-agents list signing. +# +# The PRIVATE key is written to a file with 0600 permissions and is NEVER +# printed to the terminal or any log. Only PUBLIC material is printed to +# stdout: +# - PUBLIC KEY (hex) — embed via: +# go build -ldflags "-X github.com/pilot-protocol/trustedagents.embeddedPubKeyHex=<64-hex-chars>" +# - PUBLIC KEY (base64) — the value for the optional per-entry `public_key` +# pin field in trusted-agents.json (the keyring expects std-base64). +# +# Usage: +# bash scripts/gen-signing-key.sh [output-file] +# +# output-file defaults to ./signing-key.hex. KEEP THIS FILE SECRET — it is +# the signing private key. Do NOT commit it (it is gitignored). +set -euo pipefail + +OUT_FILE="${1:-signing-key.hex}" + +if [[ -e "$OUT_FILE" ]]; then + echo "error: refusing to overwrite existing file: $OUT_FILE" >&2 + echo " remove it or pass a different output path." >&2 + exit 1 +fi + +# Resolve the output path to an absolute one before we cd into a temp dir +# to build the helper, so a relative OUT_FILE still lands where the operator +# invoked the script. +case "$OUT_FILE" in + /*) ABS_OUT="$OUT_FILE" ;; + *) ABS_OUT="$PWD/$OUT_FILE" ;; +esac + +# Create the file with 0600 perms BEFORE writing any secret into it, so the +# private key is never world/group readable, not even for an instant. +umask 077 +: > "$ABS_OUT" +chmod 600 "$ABS_OUT" + +# `go run -` (stdin) is not portable across Go versions, so materialise the +# helper in a private temp dir and run it from there. +WORKDIR="$(mktemp -d)" +trap 'rm -rf "$WORKDIR"' EXIT + +cat > "$WORKDIR/main.go" <<'EOF' +package main + +import ( + "crypto/ed25519" + "crypto/rand" + "encoding/base64" + "encoding/hex" + "fmt" + "os" +) + +func main() { + privFile := os.Getenv("PRIV_FILE") + if privFile == "" { + fmt.Fprintln(os.Stderr, "error: PRIV_FILE not set") + os.Exit(1) + } + + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + + // Write the private key (hex) to the pre-created 0600 file only. + f, err := os.OpenFile(privFile, os.O_WRONLY|os.O_TRUNC, 0o600) + if err != nil { + fmt.Fprintf(os.Stderr, "error: open private key file: %v\n", err) + os.Exit(1) + } + if _, err := fmt.Fprintln(f, hex.EncodeToString(priv)); err != nil { + _ = f.Close() + fmt.Fprintf(os.Stderr, "error: write private key: %v\n", err) + os.Exit(1) + } + if err := f.Close(); err != nil { + fmt.Fprintf(os.Stderr, "error: close private key file: %v\n", err) + os.Exit(1) + } + + // Print ONLY public material to stdout. + fmt.Printf("PRIVATE KEY written to: %s (mode 0600 — keep secret, do NOT commit)\n", privFile) + fmt.Printf("PUBLIC KEY (hex, for -ldflags embeddedPubKeyHex): %s\n", hex.EncodeToString(pub)) + fmt.Printf("PUBLIC KEY (base64, for trusted-agents.json public_key pin): %s\n", base64.StdEncoding.EncodeToString(pub)) +} +EOF + +( + cd "$WORKDIR" + GOWORK=off GO111MODULE=off PRIV_FILE="$ABS_OUT" go run main.go +) + +echo +echo "Done. The private key lives in: $ABS_OUT" +echo "Treat it as a secret: do not commit it, do not paste it into logs or chat."