Run it#
go build -o pingback .
NEXUS_CREATOR_KEY=nx_crt_... \
NEXUS_REGISTERED_HOST=mcp.you.example \
NEXUS_PUBLIC_KEY=base64... \
PORT=8080 ./pingback| Variable | Required | What it does |
|---|---|---|
NEXUS_CREATOR_KEY | Yes | The nx_crt_ value shown once when you published. Without it the process refuses to start |
NEXUS_REGISTERED_HOST | With a public key | The host you registered — not the inbound Host header |
NEXUS_PUBLIC_KEY | Yes | Enables signature verification. The process refuses to start without it, unless you set NEXUS_ALLOW_UNVERIFIED=1 on purpose |
NEXUS_ALLOW_UNVERIFIED | No | Set to 1 to run with no signature verification — a first local call, nothing else. It logs a warning on every start |
NEXUS_DEBUG_SIGNING | No | Set to 1 to print the canonical string when verification fails. Off by default: that string embeds the request path and the correlation id, which are attacker-controlled |
PORT | No | Defaults to 8080 |
The only function you replace#
Everything else in the file is the contract. This is your tool. Until you change it, it echoes the envelope — which is the most useful thing a first call can tell you.
func runTool(r *http.Request, body []byte, verdict Verdict) string {
sum := sha256.Sum256(body)
return fmt.Sprintf(
"pingback ok\n"+
" creator key: matched\n"+
" signature: %s\n"+
" correlation id: %s\n"+
" method + path: %s %s\n"+
" host as sent: %s\n"+
" body bytes: %d\n"+
" body sha256: %s\n"+
" Authorization: %s\n"+
" X-Nexus-Api-Key present: %t (must be false: the developer's key never reaches you)\n"+
" request body: %s",
verdict.SignatureStatus,
r.Header.Get("X-Correlation-ID"),
r.Method, RequestTarget(r),
r.Host,
len(body),
hex.EncodeToString(sum[:]),
describeHeader(r.Header.Get("Authorization")),
r.Header.Get("X-Nexus-Api-Key") != "",
truncate(string(body), 400),
)
}
What to try once a call lands#
A ping-back is the right shape for learning the system, because these four cases teach more than a working call does:
| Do this | Expect |
|---|---|
Return a 500 | The developer is refunded automatically, and you are not paid — the failure happened before any byte reached them |
| Sleep 10 seconds before the first byte | Cut at 5 seconds idle. The developer gets 502 Upstream_Unavailable and is refunded |
Return a 302 with a Location | Clamped to 502 and the header stripped. We never follow a redirect |
| Send the wrong creator key deliberately | Your own endpoint answers 401 — proving the check works before you rely on it |
The whole file#
385 lines, no dependencies beyond the standard library.
// A complete Nexus402 creator endpoint you can host.
//
// Clone this directory, replace `runTool` with your own logic, deploy behind
// TLS, and publish the URL. Everything else here is the contract and should be
// left alone.
//
// IT IS A PING-BACK UNTIL YOU CHANGE IT: `runTool` echoes what it received,
// including which checks passed, so your first successful call shows you the
// whole envelope rather than a bare "ok".
//
// NO DEPENDENCIES. Standard library only, which is the point: Nexus402 ships
// no SDK, and a creator endpoint needs none. This file is a reference to copy,
// not a package to import — so it can never version-skew against the gateway.
//
// go build -o pingback . && \
// NEXUS_CREATOR_KEY=nx_crt_... \
// NEXUS_REGISTERED_HOST=mcp.you.example \
// NEXUS_PUBLIC_KEY=base64... \
// PORT=8080 ./pingback
//
// The two checks, in the order they matter:
//
// 1. `X-Nexus-Creator-Key` — a constant-time compare. Proves the request came
// from Nexus402. It AUTHENTICATES on its own; it does not SECURE on its
// own, which is what the second check is for.
// 2. The Ed25519 signature — proves the request was not altered, and binds it
// to your host and to a 60-second window.
//
// BOTH ARE REQUIRED TO START. `NEXUS_PUBLIC_KEY` unset is a fatal error unless
// you also set `NEXUS_ALLOW_UNVERIFIED=1`, which is for a first local call and
// nothing else. This paragraph said the signature was "optional" and that the
// server "still serves" without it, which was true before the opt-in was added
// and false afterwards — a creator who believed it configured one variable and
// met a fatal the documentation had ruled out.
//
// Why bother with the second one, when the first authenticates? The creator key
// is a SHARED SECRET sitting on your server, sent on every request. If it leaks
// through a log, a proxy or a misconfigured CDN, anyone can impersonate
// Nexus402 to you and take free tool calls. The signature is asymmetric — you
// hold only a public key, so there is nothing on your side worth stealing.
//
// WHAT THE SIGNATURE DOES NOT DO: it narrows replay rather than closing it.
// There is no nonce and no seen-cache here, so a captured request can be
// replayed to this same host until its timestamp leaves the 60-second window.
// Closing that needs per-request state, which this example deliberately does
// not keep — if you need it, record the signed `X-Correlation-ID` and reject
// a repeat.
package main
import (
"crypto/ed25519"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
)
const (
// maxRequestBody bounds what this endpoint will read. The gateway caps
// your RESPONSE at 10 MiB; bounding the request too means a confused or
// hostile caller cannot make this process hold an unbounded buffer.
maxRequestBody = 1 << 20 // 1 MiB
// signatureWindow must match the gateway's 60 seconds. Wider accepts older
// replays; narrower rejects honest requests on ordinary clock skew.
signatureWindow = 60 * time.Second
)
// Endpoint holds the configuration a creator endpoint needs.
type Endpoint struct {
// CreatorKey is the `nx_crt_` value shown once when you published.
CreatorKey string
// RegisteredHost is the host you registered with Nexus402 — NOT the
// inbound `Host` header.
//
// THIS IS THE DETAIL INTEGRATORS GET WRONG. The signature covers the host
// the gateway SENT. Behind a proxy, a load balancer or a PaaS router the
// header your process sees may have been rewritten, and a verifier reading
// it would then reject every valid request. The registered value is a
// constant you already know.
RegisteredHost string
// PublicKey is Nexus402's Ed25519 public key. Nil disables signature
// verification; the creator-key check still runs.
PublicKey ed25519.PublicKey
}
// Verdict is what the checks concluded, so a handler can report it honestly.
type Verdict struct {
KeyMatched bool
SignatureStatus string
}
// refusesToStart is the no-public-key rule, in one place.
//
// A function rather than an inline condition so a test can assert the actual
// rule `main` uses. The opt-in must be EXACTLY "1": accepting "true", "yes"
// or any non-empty value would mean a stray or misspelled value silently
// turns verification off, which is the failure this rule exists to prevent.
func refusesToStart(publicKey, allowUnverified string) bool {
return strings.TrimSpace(publicKey) == "" && allowUnverified != "1"
}
func main() {
endpoint := Endpoint{
CreatorKey: os.Getenv("NEXUS_CREATOR_KEY"),
RegisteredHost: os.Getenv("NEXUS_REGISTERED_HOST"),
}
if endpoint.CreatorKey == "" {
log.Fatal("NEXUS_CREATOR_KEY is required: without it this endpoint cannot tell " +
"Nexus402 from anyone else who found the URL")
}
// NO PUBLIC KEY IS A REFUSAL, not a default.
//
// This used to serve happily without one, reporting the signature as "not
// checked". That is a FAIL-OPEN default in a file creators copy and host
// publicly, and the forgotten-variable case is the common one. Without the
// signature, the creator key is the only thing standing between a stranger
// and this endpoint — and it is a bearer token sent on every request, so
// one leak through a log, a proxy or a misconfigured CDN is enough. The
// signature is also what binds the request to THIS host and to a 60-second
// window, so losing it loses replay protection too.
//
// Unverified mode still exists, because it is genuinely useful for a first
// local call. It now takes an explicit opt-in, so it cannot happen by
// omission.
raw := strings.TrimSpace(os.Getenv("NEXUS_PUBLIC_KEY"))
if refusesToStart(raw, os.Getenv("NEXUS_ALLOW_UNVERIFIED")) {
log.Fatal("NEXUS_PUBLIC_KEY is required. Without it this endpoint cannot tell a " +
"request Nexus402 signed from one a stranger replayed, and the creator key " +
"alone is a bearer token that leaks through logs and proxies. To run " +
"WITHOUT signature verification on purpose — a first local call, nothing " +
"else — set NEXUS_ALLOW_UNVERIFIED=1.")
}
if raw == "" {
log.Print("WARNING: running with NO SIGNATURE VERIFICATION " +
"(NEXUS_ALLOW_UNVERIFIED=1). Every request that carries the creator key is " +
"accepted, replays included. Do not serve real traffic like this.")
}
if raw != "" {
decoded, err := base64.StdEncoding.DecodeString(raw)
if err != nil || len(decoded) != ed25519.PublicKeySize {
log.Fatalf("NEXUS_PUBLIC_KEY is not a base64 Ed25519 public key")
}
endpoint.PublicKey = ed25519.PublicKey(decoded)
if endpoint.RegisteredHost == "" {
// REFUSED rather than guessed. Falling back to the inbound header
// would work in testing and fail behind every real proxy, which is
// the worst possible time to find out.
log.Fatal("NEXUS_REGISTERED_HOST is required when NEXUS_PUBLIC_KEY is set: " +
"the host is part of the signed message, and reading it from the " +
"request header breaks behind any proxy that rewrites Host")
}
}
http.HandleFunc("/mcp", endpoint.Handle)
http.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
})
// PARSED TO AN INTEGER, and the integer is what gets used.
//
// `PORT` is environment input, and it reaches both a log line and a listen
// address. A value carrying a newline would forge log entries, so the raw
// string is never printed or concatenated: `port` holds the parsed number
// and every later use formats it with %d. Validating the string and then
// using the string would be the same bug with a check in front of it.
port := 8080
if raw := os.Getenv("PORT"); raw != "" {
n, err := strconv.Atoi(raw)
if err != nil || n < 1 || n > 65535 {
log.Fatal("PORT must be a number between 1 and 65535")
}
port = n
}
// #nosec G706 -- `port` is an int, formatted %d: it cannot carry a newline,
// so there is nothing to inject. gosec tracks that os.Getenv reached this
// call and not that strconv.Atoi stands between them. Narrowed to this one
// line rather than excluding `examples/` in .golangci.yml, because this is
// the file creators COPY and is the last place to turn a security linter off.
log.Printf("creator endpoint on :%d/mcp (signature verification: %v)",
port, endpoint.PublicKey != nil)
// These bound the INBOUND direction. The gateway's own limits on you are
// 5s to the first byte at any idle point and 120s for the whole response.
server := &http.Server{
Addr: fmt.Sprintf(":%d", port),
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 15 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}
log.Fatal(server.ListenAndServe())
}
// Handle authenticates the request, then runs the tool.
func (e Endpoint) Handle(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "POST only", http.StatusMethodNotAllowed)
return
}
body, err := io.ReadAll(io.LimitReader(r.Body, maxRequestBody+1))
if err != nil {
http.Error(w, "unreadable body", http.StatusBadRequest)
return
}
if len(body) > maxRequestBody {
http.Error(w, "body too large", http.StatusRequestEntityTooLarge)
return
}
verdict, err := e.Authenticate(r, body)
if err != nil {
// NO DETAIL IN THE BODY. A response that explains which check failed
// tells a prober how close they are. The reason goes to your log.
log.Printf("refused: %v", err)
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
// Content blocks are what the gateway relays to the developer's MCP
// client, so this shape is what they actually see.
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]any{
"content": []map[string]string{{
"type": "text",
"text": runTool(r, body, verdict),
}},
})
}
// Authenticate runs both checks and returns what it concluded.
func (e Endpoint) Authenticate(r *http.Request, body []byte) (Verdict, error) {
// CONSTANT TIME. A byte-at-a-time comparison leaks the key one position at
// a time to anyone who can measure the response.
presented := r.Header.Get("X-Nexus-Creator-Key")
if subtle.ConstantTimeCompare([]byte(presented), []byte(e.CreatorKey)) != 1 {
return Verdict{}, fmt.Errorf("creator key mismatch")
}
verdict := Verdict{KeyMatched: true, SignatureStatus: "not checked (no public key configured)"}
if e.PublicKey == nil {
return verdict, nil
}
if err := e.VerifySignature(r, body); err != nil {
return Verdict{}, err
}
verdict.SignatureStatus = "verified"
return verdict, nil
}
// VerifySignature checks the six-field Ed25519 signature.
//
// SIX fields, and the order is fixed:
//
// method \n host \n path-and-query \n timestamp \n correlation-id \n sha256(body)
//
// The host is one of them. It is what stops a request signed for one creator
// being replayed at another creator's endpoint, which matters because every
// creator verifies with the same platform key today.
func (e Endpoint) VerifySignature(r *http.Request, body []byte) error {
raw := strings.TrimPrefix(r.Header.Get("X-Nexus-Signature"), "ed25519:")
signature, err := base64.StdEncoding.DecodeString(raw)
if err != nil || len(signature) != ed25519.SignatureSize {
return fmt.Errorf("signature is not 64 base64-encoded bytes")
}
timestamp := r.Header.Get("X-Nexus-Timestamp")
seconds, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
return fmt.Errorf("timestamp %q is not unix seconds", timestamp)
}
// BOTH directions. Without an upper bound a future-dated request is valid
// forever; without a lower one a captured request replays forever.
if delta := time.Since(time.Unix(seconds, 0)); delta > signatureWindow || delta < -signatureWindow {
return fmt.Errorf("timestamp is %s outside the %s window", delta, signatureWindow)
}
message := CanonicalString(r.Method, e.RegisteredHost, RequestTarget(r), timestamp,
r.Header.Get("X-Correlation-ID"), body)
if !ed25519.Verify(e.PublicKey, message, signature) {
// THE CANONICAL STRING IS NOT LOGGED BY DEFAULT, and that is a change
// from the obvious thing to do.
//
// It embeds the request path, the query and the correlation id -- all
// attacker-controlled. Writing them to a creator’s log hands a stranger
// a pen: text of their choosing in the log an operator reads, and one
// line per failed probe, so a flood of garbage requests is a flood of
// log volume. Neither is catastrophic; both are free to avoid.
//
// So the default says only THAT it failed, with a short hash of the
// string, which is enough to tell "every request fails identically"
// from "they differ". Set NEXUS_DEBUG_SIGNING=1 to see the bytes while
// you are debugging your own deployment.
if os.Getenv("NEXUS_DEBUG_SIGNING") == "1" {
log.Printf("canonical string did not verify:\n%s", message)
} else {
sum := sha256.Sum256(message)
log.Printf("signature did not verify (canonical sha256 %x, set "+
"NEXUS_DEBUG_SIGNING=1 to print the string)", sum[:6])
}
return fmt.Errorf("ed25519 verification failed")
}
return nil
}
// CanonicalString builds the exact bytes Nexus402 signed.
func CanonicalString(method, host, target, timestamp, correlationID string, body []byte) []byte {
// The RAW bytes are hashed. Hashing a re-serialised JSON value changes the
// bytes and the hash will not match.
sum := sha256.Sum256(body)
return []byte(strings.Join([]string{
method,
host,
target,
timestamp,
correlationID,
hex.EncodeToString(sum[:]),
}, "\n"))
}
// RequestTarget is the path WITH the query, matching what the gateway signs.
func RequestTarget(r *http.Request) string {
if r.URL.RawQuery == "" {
return r.URL.EscapedPath()
}
return r.URL.EscapedPath() + "?" + r.URL.RawQuery
}
// runTool is THE ONLY FUNCTION YOU REPLACE.
//
// It receives the authenticated request and returns the text your tool
// produces. Return quickly: the gateway allows 5 seconds to your first byte at
// any idle point, and 120 seconds for the whole response.
//
// Until you change it, it echoes the envelope — which is the most useful thing
// a first call can tell you.
func runTool(r *http.Request, body []byte, verdict Verdict) string {
sum := sha256.Sum256(body)
return fmt.Sprintf(
"pingback ok\n"+
" creator key: matched\n"+
" signature: %s\n"+
" correlation id: %s\n"+
" method + path: %s %s\n"+
" host as sent: %s\n"+
" body bytes: %d\n"+
" body sha256: %s\n"+
" Authorization: %s\n"+
" X-Nexus-Api-Key present: %t (must be false: the developer's key never reaches you)\n"+
" request body: %s",
verdict.SignatureStatus,
r.Header.Get("X-Correlation-ID"),
r.Method, RequestTarget(r),
r.Host,
len(body),
hex.EncodeToString(sum[:]),
describeHeader(r.Header.Get("Authorization")),
r.Header.Get("X-Nexus-Api-Key") != "",
truncate(string(body), 400),
)
}
func describeHeader(value string) string {
if value == "" {
return "(absent)"
}
return fmt.Sprintf("(present, %d bytes — the DEVELOPER's own, passed through)", len(value))
}
func truncate(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n] + "..."
}