Documentation

SDK reference

Java SDK

io.messh:messh-sdk signs requests from your agent. io.messh.verify checks them in your API, with a filter for the JDK’s HttpServer.

Java 17+No dependenciesv0.1.0Apache-2.0

Install#

The client goes in the agent:

implementation("io.messh:messh-sdk:0.1.0")
<dependency>
  <groupId>io.messh</groupId>
  <artifactId>messh-sdk</artifactId>
  <version>0.1.0</version>
</dependency>

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

implementation("io.messh:messh-sdk:0.1.0")  // same artifact: io.messh.verify
<dependency>
  <groupId>io.messh</groupId>
  <artifactId>messh-sdk</artifactId>
  <version>0.1.0</version>
</dependency>

Java 17+. JDK only: the built-in Ed25519 provider and java.net.http.

Quick start#

Sign in the agent:

Agent.java
import io.messh.KeySource;
import io.messh.MesshClient;
import java.net.URI;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;

public class Agent {
  public static void main(String[] args) throws Exception {
    // Signs as mukul with the build-bot key. Thread-safe: create one per process.
    Path key = Path.of(System.getProperty("user.home"), ".ssh", "build-bot");
    MesshClient client = MesshClient.builder("mukul").key(KeySource.file(key)).build();

    URI uri = URI.create("https://api.acme.dev/deployments");
    byte[] body = "{\"env\":\"staging\"}".getBytes(StandardCharsets.UTF_8);
    HttpResponse<String> res =
        client.send("POST", uri, body, HttpResponse.BodyHandlers.ofString());
    System.out.println(res.statusCode());
  }
}

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

Deployments.java · JDK HttpServer
import com.sun.net.httpserver.HttpServer;
import io.messh.verify.Agent;
import io.messh.verify.MesshHttpFilter;
import io.messh.verify.MesshVerifier;
import java.net.InetSocketAddress;

public class Deployments {
  public static void main(String[] args) throws Exception {
    MesshVerifier verifier = MesshVerifier.builder().trust("mukul").build();
    HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
    server.createContext("/deployments", ex -> {
      Agent agent = (Agent) ex.getAttribute(MesshHttpFilter.AGENT);
      ex.sendResponseHeaders(can(agent, "deploy") ? 200 : 403, -1);
      ex.close();
    }).getFilters().add(new MesshHttpFilter(verifier));
    server.start();
  }
}

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

Topics#

Agent side

API side

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

Agent side#

Agent side·Builder

MesshClient.builder(identity)

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

public static MesshClient.Builder builder(String identity)
// MESSH_IDENTITY, MESSH_KEY or MESSH_KEY_FILE, MESSH_REGISTRY
public static MesshClient fromEnv()
public static MesshClient.Builder builderFromEnv()

public static final class Builder {
  public Builder key(KeySource key);            // default: ~/.ssh/id_ed25519
  public Builder registry(String url);          // default: "https://messh.io"
  public Builder expiresIn(Duration d);         // default: 60 s, max 300 s
  public Builder httpClient(HttpClient http);   // default: a new HttpClient
  public MesshClient build();
}

Parameters

identityString
Your messh username, such as mukul. Requests name it in Signature-Agent: "<registry>/<identity>".required
.key()KeySource
Where the private key comes from; see Key sources. It must be the ssh-ed25519 key you published with messh publish.~/.ssh/id_ed25519
.registry()URL
The messh registry that serves your key directory.https://messh.io
.expiresIn()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

  • Throws MesshException, unchecked (code()).
  • For agents and CI, configure the client with environment variables instead of code: MesshClient.fromEnv().
Agent side·Method

client.whoami()

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

public PublishedKey whoami()
public String fingerprint()   // SHA256:…, as messh keys and ssh-keygen -l print it

public record PublishedKey(
    String identity,      // "mukul"
    String keyName,       // "build-bot"
    String keyId,         // RFC 7638 thumbprint, sent as keyid
    String fingerprint,   // "SHA256:…"
    String agentUrl) {}   // "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.send(method, uri, body, handler)

Signs a request and sends it.

public <T> HttpResponse<T> send(
    String method, URI uri, byte[] body, HttpResponse.BodyHandler<T> handler)
    throws IOException, InterruptedException

public <T> CompletableFuture<HttpResponse<T>> sendAsync(
    String method, URI uri, byte[] body, HttpResponse.BodyHandler<T> handler)

Returns

An HttpResponse<T>. 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 hashed for Content-Digest and sent as application/json; pass null for none.
Agent side·Method

client.sign(method, uri, body)

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

public Map<String, String> sign(String method, URI uri, byte[] body)
public Map<String, String> sign(String method, URI uri, byte[] body, String host)

// a java.net.http request builder, already signed
public HttpRequest.Builder newRequest(String method, URI uri, byte[] body)

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

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.KeySource.file(path)
EnvironmentCI and servers: PEM text, or base64 of PEM or PKCS#8 DER, in an environment variable.KeySource.env("MESSH_AGENT_KEY")
SeedA raw 32-byte Ed25519 seed from your own secret store.KeySource.seed(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.KeySource.sshAgent()
Your signerA KMS or an HSM: implement the public key and sign; the SDK never sees the private key.KeySource.signer(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·HttpServer filter

MesshHttpFilter

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

public final class MesshHttpFilter extends com.sun.net.httpserver.Filter {
  public MesshHttpFilter(MesshVerifier verifier);
  // the verified Agent: exchange.getAttribute(MesshHttpFilter.AGENT)
  public static final String AGENT = "messh.agent";
}

public static MesshVerifier.Builder builder()
public static final class Builder {
  public Builder trust(String... patterns);                  // required
  public Builder registry(String origin, String fetchBase);  // default: messh.io
  public Builder maxSkew(Duration d);                        // default: 60 s
  public Builder cacheTtl(Duration d);                       // default: 5 min
  public Builder staleIfError(Duration d);                   // default: 1 h
  public Builder nonceStore(NonceStore s);                   // default: in memory
  public Builder timeout(Duration d);                        // default: 5 s
  public Builder maxBodyBytes(long n);                       // default: 1 MiB
  public MesshVerifier build();
}

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
.registry()String, String
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
.maxSkew()Duration
How far created may be from this server’s clock, either way. Nonces are remembered for twice this long.60 s
.cacheTtl()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
.staleIfError()Duration
How long cached keys keep working while messh is unreachable.1 h
.nonceStore()NonceStore
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.
  • For a Servlet container, call MesshVerifier.verify from a jakarta.servlet.Filter the same way.
API side·Method

MesshVerifier.verify(method, uri, headers, body)

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

public Agent verify(
    String method, URI uri, Map<String, List<String>> headers, byte[] body)
// throws VerifyException (unchecked)

Returns

The verified Agent.

Discussion

  • Thrown as VerifyException, unchecked, with 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·Record

io.messh.verify.Agent

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

public record Agent(
    String identity,      // "mukul"
    String keyName,       // "build-bot": the key's name on messh
    String keyId,         // RFC 7638 thumbprint
    String fingerprint,   // "SHA256:…"
    String agentUrl,      // "https://messh.io/mukul"
    long created) {}      // unix seconds

Fields

identity()string
The signer’s messh username. Use it in your authorization checks.
keyName()string
The key’s name on messh, such as laptop or build-bot. An agent is a named key.
keyId()string
Exactly which key signed: its RFC 7638 thumbprint. Log it.
fingerprint()string
The SSH fingerprint, as messh resolve prints it.
agentUrl()string
The Signature-Agent the request named.
created()time
When the request was signed.

Errors#

In the agent. Throws MesshException, unchecked (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. Thrown as VerifyException, unchecked, with 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