API
Two protocol surfaces
EON Path has two distinct APIs, and it’s easy to conflate them:
- 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.
- 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_ADDRon the server, default0.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:
- 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.
- 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.
- Key exchange. The client encapsulates a shared secret against the server’s signed, ephemeral ML-KEM-1024 public key.
- 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/apiThe 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
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /api/health | none | Health check. |
| GET | /api/algorithms | none | List supported PQC algorithms. |
| POST | /api/auth/register | none | Create a new user account. |
| POST | /api/auth/login | none | Authenticate and receive a bearer token. |
| GET | /publickey/:userid | none | Get a user’s primary public key. |
| GET | /publickeys/:userid | none | Get all of a user’s public keys. |
| GET | /api/keys/user/:username | none | Get public keys by username. |
| GET | /api/keys/fingerprint/:fingerprint | none | Get a key by its fingerprint (used by the VPN handshake, see above). |
| GET | /api/keys/search | none | Search public keys by username or comment. |
| GET | /api/me | bearer | Get the current user’s profile. |
| PUT | /api/me | bearer | Update the current user’s profile. |
| POST | /api/me/password | bearer | Change the current user’s password. |
| GET | /api/my/keys | bearer | List the current user’s public keys. |
| POST | /api/my/keys | bearer | Upload a new public key. |
| PUT | /api/my/keys/:id | bearer | Update key metadata (for example, its comment). |
| POST | /api/my/keys/:id/revoke | bearer | Mark a key as revoked. |
| DELETE | /api/my/keys/:id | bearer | Permanently delete a key. |
| POST | /publickey | bearer | Upload a public key (legacy endpoint). |
| GET | /api/my/ip | bearer | Get the current user’s VPN IP lease. |
| POST | /api/my/ip | bearer | Request a new IP lease. |
| DELETE | /api/my/ip | bearer | Release the current IP lease. |
| POST | /endpoints | signed payload | Endpoint 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 | /endpoints | none | List 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/registerRequest:
{
"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/loginRequest:
{
"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"
}| Code | Meaning |
|---|---|
| 400 | Bad request — invalid input |
| 401 | Unauthorized — missing or invalid token |
| 403 | Forbidden — insufficient permissions |
| 404 | Not found |
| 409 | Conflict — resource already exists |
| 429 | Too many requests — rate limit exceeded |
| 500 | Internal server error |
All endpoints are rate-limited per client IP, with a lower limit on the authentication endpoints.