Skip to main content

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​

SurfaceDefault portCredentialWho is meant to call it
Node /v1/*3413Basic, api_secret_path, on every route except GET /v1/versionYou, from the same machine
Node /v2/owner3413Basic, api_secret_path. A single plain get_status call is answered without itYou, from the same machine
Node /v2/foreign3413Optional Basic, foreign_api_secret_pathAnything that reads the chain
Wallet Owner /v3/owner3420Basic, the wallet's api_secret_path, and a token from open_wallet inside the encrypted envelopeYou, from the same machine
Wallet Owner /v2/owner3420Basic, the wallet's api_secret_path. Superseded by /v3/ownerNothing. Bind the listener to loopback
Wallet Foreign /v2/foreign3415NoneA 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_status on /v2/owner, sent alone rather than in a batch, with params of [] or null and 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_path commented 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 /v1 and /v2/owner; it does not open them.
  • An empty .api_secret is 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).
  • OPTIONS is 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
epic-wallet 4.1.0 cannot use a node with a foreign secret

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.

Next​