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.
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:
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.
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
MesshClient.builder(identity)Loads the signing key once and returns a client that signs every request it sends.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.client.send(method, uri, body, handler)Signs a request and sends it.client.sign(method, uri, body)Keeps your own HTTP stack. Returns, or adds, the signature headers for requests you build yourself.KeySourceWhere the private key comes from. The default is~/.ssh/id_ed25519. Keys are used locally and never go to messh.
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#
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 inSignature-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().
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.
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,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 hashed for
Content-Digestand sent asapplication/json; passnullfor none.
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
hostif you send a Host header that differs from the URL’s. - Signing is local and takes microseconds; there is no network call.
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. | KeySource.file(path) |
| Environment | CI and servers: PEM text, or base64 of PEM or PKCS#8 DER, in an environment variable. | KeySource.env("MESSH_AGENT_KEY") |
| Seed | A raw 32-byte Ed25519 seed from your own secret store. | KeySource.seed(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. | KeySource.sshAgent() |
| Your signer | A 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#
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 403untrusted.required .registry()String, String- 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 .maxSkew()Duration- How far
createdmay 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
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. - For a Servlet container, call
MesshVerifier.verifyfrom ajakarta.servlet.Filterthe same way.
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, withcode()andstatus(). - 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.
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
laptoporbuild-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 resolveprints it. agentUrl()string- The
Signature-Agentthe 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.