SDK reference
Go SDK
messh.io/go/messh signs requests from your agent. Its verify package checks them in any net/http server.
Install#
The client goes in the agent:
$ go get messh.io/go/[email protected]
The verifier goes in your API. It’s in the same package:
# 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:
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.
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#
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 inSignature-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().
(*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.
(*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,Signatureand, with a body,Content-Digest. Your own headers are kept. - Every call signs afresh, with a new nonce and
createdtime, so a retry is never a replay. - The body is read into memory to hash it for
Content-Digest;GetBodyis set so it can be resent.
(*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
hostif you send a Host header that differs from the URL’s. - Signing is local and takes microseconds; there is no network call.
messh.KeySource
Where the private key comes from. The default is ~/.ssh/id_ed25519. Keys are used locally and never go to messh.
| Source | Use it for | In code |
|---|---|---|
| Key file | The ssh-ed25519 key you published: an unencrypted OpenSSH key, or PKCS#8 PEM. | messh.KeyFile(path) |
| Environment | CI and servers: PEM text, or base64 of PEM or PKCS#8 DER, in an environment variable. | messh.KeyEnv("MESSH_AGENT_KEY") |
| Seed | A raw 32-byte Ed25519 seed from your own secret store. | messh.KeySeed(seed) |
| ssh-agent | A 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 signer | A 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#
(*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 403untrusted.required Registryorigin, fetchBase- Accepted
Signature-Agentorigins, optionally mapped to where key directories are fetched, such as an internal hostname. Agents from other origins get 403untrustedand are never fetched.https://messh.io MaxSkewtime.Duration- How far
createdmay 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
trustare 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 413body_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()).
(*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; useerrors.As, then.Codeand.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.
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
laptoporbuild-bot. An agent is a named key. KeyIDstring- Exactly which key signed: its RFC 7638 thumbprint. Log it.
Fingerprintstring- The SSH fingerprint, as
messh resolveprints it. AgentURLstring- The
Signature-Agentthe 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.