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 calls the management API to confirm a connecting client’s public key is registered and active (see step 4 below).

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

  • Transport: plain 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).
  • Framing: a custom, length-prefixed binary protocol (4-byte big-endian length, then the payload) — not HTTP, so there are no JSON request/response bodies for this part of the API.
  • Why ML-DSA-87 and ML-KEM-1024: see Post-Quantum Cryptography.

Challenge request

The first phase authenticates the client’s identity key to the server:

StepDirectionMessageDescription
1Client → ServerProtocol version (1 byte)Identifies the handshake version in use.
2Client → ServerML-DSA-87 public key (2,592 bytes)The client’s identity key.
3Client → ServerAPI server URLTells the server which management API to check the key against.
4Server → Management APIGET /api/keys/fingerprint/:fingerprintServer looks up the client’s key fingerprint to confirm it’s registered and active.
5Server → ClientAuth result (1 byte)0x01 if the key is registered and active, 0x00 otherwise. The server closes the connection on 0x00.
6Server → ClientChallenge nonce (32 random bytes)
7Client → ServerML-DSA-87 signature (4,627 bytes)The client signs a domain-separated hash of the protocol version, its own public key, and the nonce, using its ML-DSA-87 private key.
8Server → ClientVerification result (1 byte)0x01 if the signature verifies against the public key from step 2, 0x00 otherwise. The server closes the connection on failure.

Session establishment

Once the client is authenticated, the server proves its own identity and both sides derive the key material for the WireGuard tunnel:

StepDirectionMessageDescription
9Server → ClientServer ML-DSA-87 identity key, ephemeral ML-KEM-1024 public key, and a signature over both plus the prior transcriptLets the client verify the key came from the server it expects, not an on-path attacker.
10Client (local)Client verifies the server’s ML-DSA-87 identity against a pinned hash or DNS TXT record configured for the endpoint. See Server identity pinning — this step fails closed if neither is configured.
11Client (local)Client verifies the signature from step 9, then encapsulates a shared secret against the server’s ML-KEM-1024 public key.
12Client → ServerML-KEM-1024 ciphertext
13Both sides (local)Both sides derive the WireGuard pre-shared key from the shared secret, bound to a hash of the full handshake transcript (including the ciphertext from step 12). Any tampering with an earlier message produces mismatched keys on the two sides, so a tampered handshake fails closed instead of silently succeeding.
14Both sidesWireGuard public keysThe two sides exchange WireGuard public keys; the server adds the client as a peer, and the WireGuard tunnel comes up using the derived pre-shared key.

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.
GET/metricsnonePrometheus metrics.
POST/GET/legacy/publickey, /legacy/publickeys/:userid, /legacy/publickey/:userid[/:keyname]mixedFile-based public-key endpoints kept for old clients; new clients use /api/keys/*.

Endpoint registration

POST /endpoints replaced an older POST /endpoints/:name, which was trust-on-first-use — whatever X-API-Key the first caller sent became the key for that name. Registration is now 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. Replays are bounded by a nonce cache.

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.

Administrative and billing routes

Two route groups exist for EON Core, not for tenants, and each is registered only when its token is configured — without the token the routes don’t exist rather than refusing. Both take the token in the X-Admin-Token header.

MethodPathTokenPurpose
POST/GET/api/admin/organizationsadminCreate and list organizations.
POST/DELETE/GET/api/admin/organizations/:id/prefixes[/:prefix]adminReserve, release and list the endpoint-name prefixes an organization may register under.
POST/GET/api/admin/licensesadminIssue and list licences.
PATCH/api/admin/licenses/:idadminAmend a licence in place, instead of reissuing the key.
POST/api/admin/licenses/:id/revokeadminRevoke a licence.
GET/api/admin/endpointsadminList every registered endpoint.
DELETE/api/admin/endpoints/:idadminRemove a registered endpoint.
GET/api/admin/billing/summaryadmin or billingBilling digest over the endpoint transition log.
GET/api/admin/billing/endpoints/:name/historyadmin or billingThe transition history for one endpoint.

The billing group accepts a second, read-only token (EONPATH_BILLING_TOKEN) so an invoicing job never holds a token that can issue or revoke a licence. Neither token replaces restricting /api/admin at the reverse proxy — a shared secret on a published path is one leak away from a self-issued licence.

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

Authentication endpoints are rate-limited to 5 requests/minute; other endpoints to 30 requests/minute.