Documentation

SDK reference

Go SDK

messh.io/go/messh signs requests from your agent. Its verify package checks them in any net/http server.

Go 1.22+No dependenciesv0.1.0Apache-2.0

Install#

The client goes in the agent:

Terminal
$ go get messh.io/go/[email protected]

The verifier goes in your API. It’s in the same package:

Terminal
# same module: import "messh.io/go/messh/verify"
$ go get messh.io/go/[email protected]

Go 1.22+. Standard library only.

Quick start#

Sign in the agent:

main.go
package main

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"messh.io/go/messh"
)

func main() {
	// Signs as mukul with the build-bot key. Create one client per process.
	home, _ := os.UserHomeDir()
	key := messh.KeyFile(filepath.Join(home, ".ssh", "build-bot"))
	client, err := messh.NewClient("mukul", messh.WithKey(key))
	if err != nil {
		panic(err)
	}
	url := "https://api.acme.dev/deployments"
	body := strings.NewReader(`{"env":"staging"}`)
	resp, err := client.Post(url, "application/json", body)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	fmt.Println(resp.Status)
}

Verify in your API. can() stands for your own authorization: messh says who, you decide what.

main.go · net/http
package main

import (
	"net/http"

	"messh.io/go/messh/verify"
)

func main() {
	v, err := verify.New(verify.Trust("mukul"))
	if err != nil {
		panic(err)
	}
	mux := http.NewServeMux()
	mux.HandleFunc("POST /deployments", func(w http.ResponseWriter, r *http.Request) {
		agent := verify.AgentFrom(r.Context()) // Identity "mukul", KeyName "build-bot"
		if !can(agent, "deploy") {
			http.Error(w, "forbidden", http.StatusForbidden)
			return
		}
		w.Write([]byte(`{"ok":true}`))
	})
	http.ListenAndServe(":8080", v.Middleware(mux))
}

For the whole setup, from publishing the agent’s key to choosing who to trust, see API authentication.

Topics#

Agent side

  • messh.NewClient(identity, opts...)Loads the signing key once and returns a client that signs every request it sends.
  • (*Client).WhoAmI(ctx)Checks that the key is published and active on messh, and shows how APIs will see the agent. Call it once at startup.
  • (*Client).Do(req)Signs a request and sends it.
  • (*Client).Sign(req)Keeps your own HTTP stack. Returns, or adds, the signature headers for requests you build yourself.
  • messh.KeySourceWhere the private key comes from. The default is ~/.ssh/id_ed25519. Keys are used locally and never go to messh.

API side

  • (*Verifier).Middleware(next)Checks every incoming request before your handlers run. Valid requests carry the verified agent; everything else gets a JSON 401 or 403.
  • (*Verifier).Verify(r)Verifies one request and returns who signed it. Use it where middleware doesn’t fit: serverless handlers, webhooks, queue consumers.
  • verify.AgentWho signed the request. messh tells you who; what they may do is up to your code.

Agent side#

Agent side·Function

messh.NewClient(identity, opts...)

Loads the signing key once and returns a client that signs every request it sends.

func NewClient(identity string, opts ...Option) (*Client, error)
// MESSH_IDENTITY, MESSH_KEY or MESSH_KEY_FILE, MESSH_REGISTRY; options win
func NewClientFromEnv(opts ...Option) (*Client, error)

func WithKey(k KeySource) Option            // default: $HOME/.ssh/id_ed25519
func WithRegistry(url string) Option        // default: "https://messh.io"
func WithExpiry(d time.Duration) Option     // default: 60 s, max 300 s
func WithHTTPClient(h *http.Client) Option  // default: 30 s timeout

Parameters

identitystring
Your messh username, such as mukul. Requests name it in Signature-Agent: "<registry>/<identity>".required
WithKeyKeySource
Where the private key comes from; see Key sources. It must be the ssh-ed25519 key you published with messh publish.~/.ssh/id_ed25519
WithRegistryURL
The messh registry that serves your key directory.https://messh.io
WithExpirytime.Duration
How long each signature is valid, 1 to 300 s. Verifiers also reject anything outside their clock-skew window.60 s

Returns

A client that is safe to share across threads. Create one per process and reuse it.

Errors

invalid_identity Not a messh username: 3 to 30 lowercase letters, digits and inner hyphens; or, from the environment, MESSH_IDENTITY isn’t set.
invalid_key The key can’t be read, is passphrase-protected or isn’t Ed25519. Load a passphrase-protected key into ssh-agent; the message says what to do.
invalid_option The registry isn’t an http(s) URL, or the expiry is outside 1 to 300 s.

Discussion

  • Returns *messh.Error (.Code).
  • For agents and CI, configure the client with environment variables instead of code: NewClientFromEnv().
Agent side·Method

(*Client).WhoAmI(ctx)

Checks that the key is published and active on messh, and shows how APIs will see the agent. Call it once at startup.

func (c *Client) WhoAmI(ctx context.Context) (*PublishedKey, error)
// SHA256:…, as messh keys and ssh-keygen -l print it
func (c *Client) Fingerprint() string

type PublishedKey struct {
	Identity    string // "mukul"
	KeyName     string // "build-bot"
	KeyID       string // RFC 7638 thumbprint, sent as keyid
	Fingerprint string // "SHA256:…"
	AgentURL    string // "https://messh.io/mukul"
}

Returns

The key as messh lists it: the identity and key name an API’s verifier will report.

Errors

not_published The key isn’t an active key of the identity, or there’s no such user. Every API would answer 401 unknown_key; publish it with messh publish.
registry_unreachable messh couldn’t be reached, or didn’t answer with a key directory.

Discussion

  • One request to the registry for the identity’s key directory; nothing is signed or sent to an API.
  • Agents can run it at startup and stop with a clear message instead of failing on every call.
Agent side·Method

(*Client).Do(req)

Signs a request and sends it.

func (c *Client) Do(req *http.Request) (*http.Response, error)
func (c *Client) Get(url string) (*http.Response, error)
func (c *Client) Post(url, contentType string, body io.Reader) (*http.Response, error)

Returns

An *http.Response; close its body. A 401 or 403 from the API is a normal response, not an error.

Discussion

  • Adds Signature-Agent, Signature-Input, Signature and, with a body, Content-Digest. Your own headers are kept.
  • Every call signs afresh, with a new nonce and created time, so a retry is never a replay.
  • The body is read into memory to hash it for Content-Digest; GetBody is set so it can be resent.
Agent side·Method · RoundTripper

(*Client).Sign(req)

Keeps your own HTTP stack. Returns, or adds, the signature headers for requests you build yourself.

func (c *Client) Sign(req *http.Request) error
func (c *Client) Transport(base http.RoundTripper) http.RoundTripper

// any *http.Client
hc := &http.Client{Transport: client.Transport(nil)}

Discussion

  • Sign last. Changing the method, URL, a covered header or the body afterwards breaks the signature.
  • Pass host if you send a Host header that differs from the URL’s.
  • Signing is local and takes microseconds; there is no network call.
Agent side·Key sources

messh.KeySource

Where the private key comes from. The default is ~/.ssh/id_ed25519. Keys are used locally and never go to messh.

SourceUse it forIn code
Key fileThe ssh-ed25519 key you published: an unencrypted OpenSSH key, or PKCS#8 PEM.messh.KeyFile(path)
EnvironmentCI and servers: PEM text, or base64 of PEM or PKCS#8 DER, in an environment variable.messh.KeyEnv("MESSH_AGENT_KEY")
SeedA raw 32-byte Ed25519 seed from your own secret store.messh.KeySeed(seed)
ssh-agentA passphrase-protected key, or one held by 1Password, Secretive or a YubiKey agent. The key stays in the agent; with several loaded, pick yours by fingerprint.messh.SSHAgent("")
Your signerA KMS or an HSM: implement the public key and sign; the SDK never sees the private key.messh.KeySigner(s)

OS keychains, cloud secret managers and PKCS#11 aren’t built in. Any of them can sign through your own signer.

API side#

API side·Middleware

(*Verifier).Middleware(next)

Checks every incoming request before your handlers run. Valid requests carry the verified agent; everything else gets a JSON 401 or 403.

func New(opts ...Option) (*Verifier, error)   // Trust(...) is required
func (v *Verifier) Middleware(next http.Handler) http.Handler

func Trust(patterns ...string) Option
func Registry(origin, fetchBase string) Option   // default: https://messh.io
func MaxSkew(d time.Duration) Option             // default: 60 * time.Second
func CacheTTL(d time.Duration) Option            // default: 5 * time.Minute
func StaleIfError(d time.Duration) Option        // default: time.Hour
func Nonces(s NonceStore) Option                 // default: in memory
func HTTPClient(h *http.Client) Option           // default: 5 s timeout
func MaxBodyBytes(n int64) Option                // default: 1 MiB, then 413

Parameters

Trust...string
Who may call this API: messh usernames, or "*" for anyone with a valid messh signature. Valid signatures from anyone else get 403 untrusted.required
Registryorigin, fetchBase
Accepted Signature-Agent origins, optionally mapped to where key directories are fetched, such as an internal hostname. Agents from other origins get 403 untrusted and are never fetched.https://messh.io
MaxSkewtime.Duration
How far created may be from this server’s clock, either way. Nonces are remembered for twice this long.60 s
CacheTTLtime.Duration
How long a key directory is reused. A revoked key leaves the directory at once, so this bounds how long it can still verify. An unknown keyid refetches sooner, at most every 30 s, so a newly published key works right away.5 min
StaleIfErrortime.Duration
How long cached keys keep working while messh is unreachable.1 h
NoncesNonceStore
Remembers nonces to stop replays. In memory suits one instance; behind a load balancer, implement the one-method interface on a shared store such as Redis.in memory

Discussion

  • Callers outside trust are refused before any key lookup, so they can’t make your API fetch directories. One fetch per directory runs at a time.
  • Bodies over MaxBodyBytes (1 MiB) get 413 body_too_large.
  • Unsigned requests get 401 with Accept-Signature, so clients learn what to send (RFC 9421 §5.1).
  • Errors are JSON: {"error": "unknown_key", "message": "…"}. See Errors.
  • Handlers get the caller with verify.AgentFrom(r.Context()).
API side·Method

(*Verifier).Verify(r)

Verifies one request and returns who signed it. Use it where middleware doesn’t fit: serverless handlers, webhooks, queue consumers.

func (v *Verifier) Verify(r *http.Request) (*Agent, error)
// Reads r.Body to check Content-Digest and leaves it readable for your handler.

Returns

The verified Agent.

Discussion

  • Returned as *verify.Error; use errors.As, then .Code and .Status.
  • The authority is the Host header if present, else the URL’s host.
  • Nothing is sent to the client for you: return your own 401 or 403 with the error code.
API side·Struct

verify.Agent

Who signed the request. messh tells you who; what they may do is up to your code.

type Agent struct {
	Identity    string    // "mukul"
	KeyName     string    // "build-bot": the key's name on messh
	KeyID       string    // RFC 7638 thumbprint
	Fingerprint string    // "SHA256:…"
	AgentURL    string    // "https://messh.io/mukul"
	Created     time.Time
}

func AgentFrom(ctx context.Context) *Agent

Fields

Identitystring
The signer’s messh username. Use it in your authorization checks.
KeyNamestring
The key’s name on messh, such as laptop or build-bot. An agent is a named key.
KeyIDstring
Exactly which key signed: its RFC 7638 thumbprint. Log it.
Fingerprintstring
The SSH fingerprint, as messh resolve prints it.
AgentURLstring
The Signature-Agent the request named.
Createdtime
When the request was signed.

Errors#

In the agent. Returns *messh.Error (.Code): invalid_identity, invalid_key, invalid_option or sign_failed; whoami adds not_published and registry_unreachable. A 401 or 403 from the API is a normal response, not an error.

In your API. Returned as *verify.Error; use errors.As, then .Code and .Status.

The codes, their causes and fixes are the same in every SDK; see Errors.

Updated Oct 22, 2026

↑ ↓ to move↵ to openesc to close