API authentication and TLS
The node protects its owner-facing surfaces with HTTP Basic against a secret file on disk. The wallet protects its Owner API the same way, against a secret file of its own, and Owner API v3 also needs a token obtained with the wallet password. The foreign surfaces take no credential, because an interactive transfer needs the receiving side reachable by a stranger. TLS termination is left to the operator.
The surfaces
| Surface | Default port | Credential | Who is meant to call it |
|---|---|---|---|
Node /v1/* | 3413 | Basic, api_secret_path, on every route except GET /v1/version | You, from the same machine |
Node /v2/owner | 3413 | Basic, api_secret_path. A single plain get_status call is answered without it | You, from the same machine |
Node /v2/foreign | 3413 | Optional Basic, foreign_api_secret_path | Anything that reads the chain |
Wallet Owner /v3/owner | 3420 | Basic, the wallet's api_secret_path, and a token from open_wallet inside the encrypted envelope | You, from the same machine |
Wallet Owner /v2/owner | 3420 | Basic, the wallet's api_secret_path. Superseded by /v3/owner | Nothing. Bind the listener to loopback |
Wallet Foreign /v2/foreign | 3415 | None | A counterparty paying you |
The wallet's Foreign API receives a slate, returns a partially signed one, builds a coinbase for a miner configured to pay this wallet, and reports its version. Restrict access to it at the proxy.
The node's /v1 credential covers the whole prefix, including the chain reads that are open on
/v2/foreign. The one exception is an exact GET /v1/version, which answers anyone; POST to the same
path still needs the credential. See the v1 REST surface.
api_secret_path names a different file in each binary's configuration. Both node surfaces read the
node's, which is .api_secret. The wallet's names .owner_api_secret, which protects the wallet's own
Owner listener.
Owner access on the wallet
/v3/owner has two gates, and a call has to pass both.
The first is HTTP Basic on the listener. Username epic, password the contents of the wallet's
.owner_api_secret, realm EpicOwnerAPI. It covers everything on the Owner listener except
/v2/foreign, so /v3/owner and /v2/owner alike, and it is checked before any method runs
(controller/src/controller.rs:143). Comment out the wallet's
api_secret_path and this layer is not installed.
The second is a token. Every method in the v3 trait takes one
(api/src/owner_rpc_s.rs:78), and getting one means completing the
ECDH handshake and then calling open_wallet with the wallet password. See
the encrypted handshake.
The first call establishes the shared key. It carries the Basic credential and nothing else:
curl -s -u epic:$(cat ~/.epic/main/.owner_api_secret) \
-d '{"jsonrpc":"2.0","method":"init_secure_api","params":{"ecdh_pubkey":"<your compressed public key, hex>"},"id":1}' \
http://127.0.0.1:3420/v3/owner
Without -u the listener answers 401 before the handshake starts. Swap ~/.epic/main for the wallet
directory if the wallet lives elsewhere.
On top of that credential, every method on /v3/owner takes a token, and a token comes from
open_wallet, which needs the wallet password. owner_api_interface defaults to 127.0.0.1;
for remote access, tunnel to loopback. The listener is
controller/src/controller.rs:123.
The full method list, the encrypted envelope, and which calls move value are on the wallet Owner API page.
The node credential
The node's /v1 prefix and /v2/owner use HTTP Basic with the username literally epic and a secret
file's contents as the password:
Authorization: Basic base64("epic:" + <secret file contents>)
The node creates ~/.epic/<network>/.api_secret on first run and points api_secret_path at it, so
this works without configuration:
curl -u epic:$(cat ~/.epic/main/.api_secret) \
-d '{"jsonrpc":"2.0","method":"get_status","params":[],"id":1}' \
http://127.0.0.1:3413/v2/owner
Secret files hold one 20-character random alphanumeric string with no trailing newline
(config/src/config.rs:84), and the comparison is constant time
(basic_auth_matches()). The realm is
EpicAPI on the owner surface and EpicForeignAPI on the foreign one. A rejected call, on either
binary, returns 401 with a WWW-Authenticate header and a JSON-RPC error body:
{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Unauthorized"}, "id": null}
Two calls are answered without the credential:
GET /v1/version, exactly that method and path (api/src/handlers.rs:118).- One
get_statuson/v2/owner, sent alone rather than in a batch, withparamsof[]ornulland a body of at most 4 KiB. It returns the same status as an authenticated call. Any other method, a batch, or a body that does not parse gets 401 (is_legacy_status_request()).
The rest of this behaviour follows from the node failing closed:
- With
api_secret_pathcommented out, or pointing at a file that is not there, there is no secret to match, so every protected call returns 401 to everyone, with or without-u. Only the two calls above still answer. Commenting the key out locks/v1and/v2/owner; it does not open them. - An empty
.api_secretis regenerated with a new value rather than read as "no secret", so emptying the file does not switch authentication off (config/src/config.rs:96). OPTIONSis passed through before the credential check, which is what makes CORS preflight work.
Optional credential on the node's foreign surface
foreign_api_secret_path makes the node's /v2/foreign require a credential too. Create the secret
yourself and give it an absolute path:
head -c 40 /dev/urandom | base64 | tr -dc 'A-Za-z0-9' | head -c 20 > ~/.epic/main/.foreign_api_secret
[server]
foreign_api_secret_path = "/home/you/.epic/main/.foreign_api_secret"
The realm is EpicForeignAPI and the username is still epic. Verify with:
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://127.0.0.1:3413/v2/foreign
# 401 once the secret is in place
The wallet never sends a credential to the node's /v2/foreign, whatever is in its own .api_secret, so
every chain read it makes gets 401 and it stops with Unauthorized (HTTP 401). The error suggests
checking node_api_secret_path, which cannot help. On any node a wallet talks to, do not create the
file foreign_api_secret_path names (the key is set by default and is harmless while the file is
absent), or comment the key out, and restrict the foreign surface at a proxy instead.
The credential the wallet presents to the node
node_api_secret_path points the other way: it is the node secret the wallet holds, and it defaults to
<wallet dir>/.api_secret. The wallet sends it only on /v2/owner calls other than get_status, and
never to /v2/foreign or /v2/tor
(impls/src/node_clients/http.rs:115).
In practice that is one call, get_onion_addresses, which the wallet makes when finalising an epicbox
transfer so it can post over Tor. A missing or mismatched secret makes that lookup come back empty and
the wallet posts the normal way. Everything else the wallet asks a node, balances, outputs, kernels,
posting a transaction and the sync check, goes to surfaces that need no credential. The sync check is
the foreign ready_for_txs, and only a node that does not have that method gets the unauthenticated
get_status instead.
So a wallet whose .api_secret differs from the node's still works. A 401 from the node points
elsewhere: at a foreign_api_secret_path on the node, covered above, or at a node older than
4.1.0. That second case is read from the code, not tested: a 4.0.x node with a secret puts
all of /v2/owner behind Basic auth, so it refuses the wallet's unauthenticated get_status.
TLS
Both binaries take the same pair, both commented out by default:
tls_certificate_file = "/path/to/fullchain.pem"
tls_certificate_key = "/path/to/privkey.pem"
Setting the certificate switches the listener from HTTP to HTTPS. Setting it without the key is a startup error. Certificates are read as a PEM chain; keys may be PKCS#1, PKCS#8 or SEC1 PEM. A file that cannot be opened, cannot be parsed, or does not match its certificate stops the process rather than falling back to HTTP, so a bad pair fails loudly at startup.
The bundled epic client subcommands address the node over http://
(src/bin/cmd/client.rs:160). Use curl against a TLS listener.