Skip to content

API

Two protocol surfaces

EON Path has two distinct APIs, and it’s easy to conflate them:

  1. VPN authentication protocol — a raw TCP handshake that authenticates a client to the server and establishes a VPN session, using ML-DSA-87 challenge-response authentication and an ML-KEM-1024 key exchange. This is not an HTTP API.
  2. Management API — a REST/JSON API, served by eonpath-api, for registering accounts, publishing and querying ML-DSA public keys, and managing VPN IP leases. It has its own authentication (username/password login returning a bearer token) that is unrelated to the VPN handshake.

The two touch at exactly one point: during the VPN handshake, the server asks the management API whether a connecting client’s public key is registered and active.

VPN authentication protocol (ML-DSA-87 challenge-response)

  • Transport: TCP to the WireGuard routing method’s key-exchange listener — EONPATH_WG_KEX_ADDR on the server, default 0.0.0.0:9998 (see Configuration: Server). It is a binary protocol, not HTTP.
  • Why ML-DSA-87 and ML-KEM-1024: see Post-Quantum Cryptography.

The handshake runs in four stages:

  1. Client authentication. The client presents its ML-DSA-87 public key, the server checks with the management API that the key is registered and active, and the client signs a fresh server challenge with its private key.
  2. Server identity. The server presents its ML-DSA-87 identity key, and the client verifies it against the pinned hash or DNS TXT record configured for the endpoint. This step fails closed if neither is configured — see Server identity pinning.
  3. Key exchange. The client encapsulates a shared secret against the server’s signed, ephemeral ML-KEM-1024 public key.
  4. Tunnel. Both sides derive the WireGuard pre-shared key from that secret, bound to the handshake transcript, exchange WireGuard public keys, and bring up the tunnel. A tampered handshake produces mismatched keys and fails closed.

Management API

Base URL

http://localhost:8080/api

The default address matches EONPATH_API_ENDPOINT in the server’s environment file — see Configuration: Server.

Authentication

Most endpoints require a bearer token, obtained from /api/auth/login:

Authorization: Bearer <token>

This token authenticates account operations (managing your profile, uploading keys, requesting IP leases) — it is not used anywhere in the VPN authentication protocol described above, which authenticates the VPN connection itself using ML-DSA-87 signatures instead of passwords or tokens.

Endpoints

MethodPathAuthPurpose
GET/api/healthnoneHealth check.
GET/api/algorithmsnoneList supported PQC algorithms.
POST/api/auth/registernoneCreate a new user account.
POST/api/auth/loginnoneAuthenticate and receive a bearer token.
GET/publickey/:useridnoneGet a user’s primary public key.
GET/publickeys/:useridnoneGet all of a user’s public keys.
GET/api/keys/user/:usernamenoneGet public keys by username.
GET/api/keys/fingerprint/:fingerprintnoneGet a key by its fingerprint (used by the VPN handshake, see above).
GET/api/keys/searchnoneSearch public keys by username or comment.
GET/api/mebearerGet the current user’s profile.
PUT/api/mebearerUpdate the current user’s profile.
POST/api/me/passwordbearerChange the current user’s password.
GET/api/my/keysbearerList the current user’s public keys.
POST/api/my/keysbearerUpload a new public key.
PUT/api/my/keys/:idbearerUpdate key metadata (for example, its comment).
POST/api/my/keys/:id/revokebearerMark a key as revoked.
DELETE/api/my/keys/:idbearerPermanently delete a key.
POST/publickeybearerUpload a public key (legacy endpoint).
GET/api/my/ipbearerGet the current user’s VPN IP lease.
POST/api/my/ipbearerRequest a new IP lease.
DELETE/api/my/ipbearerRelease the current IP lease.
POST/endpointssigned payloadEndpoint registration: a server pushes the endpoints it facilitates, signed with its ML-DSA-87 identity key and, on the first push, its licence key.
GET/endpointsnoneList the endpoints clients may connect to. Standby endpoints are not in the list.

Endpoint registration

POST /endpoints takes a signed payload: the server’s ML-DSA-87 identity signs it, the first push also presents the licence key, and after that the signature alone authenticates the server. The URL the server registers with is part of what it signs, so it must match one of the API’s EONPATH_API_AUDIENCES entries. Replayed pushes are rejected.

An endpoint carries a state — standby or active — set on the server, not in the API (see Configuration: Server). GET /endpoints returns only the active ones. Unlicensed endpoints in a push are skipped; the rest of the push is accepted rather than the whole thing being refused.

Register

POST /api/auth/register

Request:

{
  "username": "alice",
  "email": "alice@example.com",
  "password": "a-strong-password"
}

Response (201 Created):

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "username": "alice",
  "email": "alice@example.com",
  "created_at": "2026-01-15T10:30:00Z"
}

Login

POST /api/auth/login

Request:

{
  "username": "alice",
  "password": "a-strong-password"
}

Response (200 OK):

{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "expires_at": "2026-01-16T10:30:00Z",
  "user": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "username": "alice"
  }
}

Errors

Error responses share a common shape:

{
  "error": "error message here"
}
CodeMeaning
400Bad request — invalid input
401Unauthorized — missing or invalid token
403Forbidden — insufficient permissions
404Not found
409Conflict — resource already exists
429Too many requests — rate limit exceeded
500Internal server error

All endpoints are rate-limited per client IP, with a lower limit on the authentication endpoints.