Skip to content

[WIP] feat(rpc): implement JSON-RPC authentication#1105

Draft
Micah-Shallom wants to merge 8 commits into
getfloresta:masterfrom
Micah-Shallom:feat/rpc-auth
Draft

[WIP] feat(rpc): implement JSON-RPC authentication#1105
Micah-Shallom wants to merge 8 commits into
getfloresta:masterfrom
Micah-Shallom:feat/rpc-auth

Conversation

@Micah-Shallom
Copy link
Copy Markdown
Contributor

@Micah-Shallom Micah-Shallom commented Jun 1, 2026

On startup the daemon collects every configured credential into a single Vec<RpcAuth>: a cookie entry (when no --rpc-password is set), a -rpcuser/-rpcpassword entry, and any -rpcauth entries. All entries are stored as (user, salt_hex, hash_hex); plaintext passwords are HMAC-SHA256'd and discarded.

The middleware iterates the vector at request time and returns 200 on first match, 401 with WWW-Authenticate: Basic realm="jsonrpc" otherwise. The cookie file rotates per restart and is removed on graceful shutdown.

What the in-memory vector looks like for the three configurations:

Operator runs entries after startup
florestad (default) [RpcAuth { user: "__cookie__", salt, hash }]
florestad --rpc-user=alice --rpc-password=hunter2 [RpcAuth { user: "alice", salt, hash }]
florestad --rpc-auth=bob:s$h --rpc-auth=carol:s$h [RpcAuth { user: "__cookie__", ... }, RpcAuth { user: "bob", ... }, RpcAuth { user: "carol", ... }]

floresta-cli reads the cookie automatically; --rpc-user and --rpc-password override; --rpc-cookie-file picks a non-default path.

Closes #651.

@Micah-Shallom Micah-Shallom force-pushed the feat/rpc-auth branch 2 times, most recently from 3aec2bc to 93cfa56 Compare June 1, 2026 00:55
@moisesPompilio
Copy link
Copy Markdown
Collaborator

You’re on the right track. In this case, you can create an Auth object to store the authentication information, because at the RPC layer it doesn’t need to know whether the user set a username and password, is using no authentication, or is using the default authenticated mode. The only thing it needs to do is, for each request, take the provided information and pass it to Auth, and then Auth decides whether the request is authorized or not.

So this Auth object should have three initialization methods: one for when authentication is disabled, one for when the user provides a username and password, and another for the default mode, where each initialization generates a new .cookie file.

After that, it should have a method like valid_login that receives two Option<String> values for the username and password. Then it can return a bool or (), and you can design custom errors for each situation that may happen so you can give the user the correct response.

@jaoleal jaoleal added the RPC Changes something with our JSON-RPC interface label Jun 1, 2026
@jaoleal jaoleal added this to Floresta Jun 1, 2026
@github-project-automation github-project-automation Bot moved this to Backlog in Floresta Jun 1, 2026
@jaoleal jaoleal moved this from Backlog to In progress in Floresta Jun 1, 2026
@luisschwab
Copy link
Copy Markdown
Member

@Micah-Shallom see https://github.com/bitcoindevkit/bdk-bitcoind-client/blob/master/src/bitreq.rs#L31-L59

@luisschwab
Copy link
Copy Markdown
Member

Also needs rebase pinning base64 to =X.Y.z.

@Micah-Shallom
Copy link
Copy Markdown
Contributor Author

@luisschwab just noticed ur recent PR merged...will update this accordingly....also looking into the bdk example shared

Write <datadir>/.cookie on startup as a single line
__cookie__:<64-char-hex-token>, no trailing newline. Token is 32 bytes
from rand::rng(), lowercase hex-encoded. File mode 0600 on Unix via
OpenOptionsExt; atomic publish via tmp + rename.

Refs: getfloresta#651
Track cookie ownership via a cookie_generated OnceLock on Florestad:
set after generate_cookie succeeds at startup, checked at
wait_shutdown so we only remove a cookie this process wrote.

delete_cookie treats NotFound as success, so shutdown stays idempotent
across crashes and double-stops.

Refs: getfloresta#651
Add parse_basic_auth_header and BasicAuthHeaderError. The parser
matches Bitcoin Core's wire semantics: case-sensitive "Basic " prefix,
whitespace-trim around the base64 payload, and split on the first ':'
so passwords may contain colons.

An axum middleware reads the Authorization header on each request,
parses it, and logs the parsed username at debug level. Missing,
non-ASCII, or malformed headers are also logged at debug. The
middleware always passes the request through; this layer does not
reject anything.

Refs: getfloresta#651
@Micah-Shallom Micah-Shallom force-pushed the feat/rpc-auth branch 2 times, most recently from ddefb98 to 31d8e95 Compare June 3, 2026 14:54
The JSON-RPC middleware now gates every request: missing, malformed,
or non-matching Authorization: Basic headers return HTTP 401 with
WWW-Authenticate: Basic realm="jsonrpc" per RFC 7235.

generate_cookie returns the cookie line again so Florestad can wrap
it in Arc<Credentials::Cookie> and pass it through to the middleware
state. Credentials::matches compares format!("{user}:{pass}") against
the stored line via a hand-rolled constant_time_eq that runs the full
length regardless of where the first mismatch lies.

floresta-cli reads <datadir>/[<net>/].cookie by default; --rpc-user +
--rpc-password override; --rpc-cookie-file picks a non-default path.
The Python test framework reads the cookie after the RPC socket opens.

Refs: getfloresta#651
Add rpc_user/rpc_password to florestad::Config, exposed via
--rpc-user/--rpc-password CLI flags and an [rpc] section in the TOML
config file. CLI values take precedence over file values.

Rename Credentials to Auth and add an Auth::UserPass variant that
stores the user:pass pair as a single pre-formatted line, sharing the
constant-time matches() path with Auth::Cookie.

When rpc_password is set, Florestad::start uses Auth::UserPass and
skips cookie generation, mirroring Bitcoin Core's mutual exclusion
between cookie auth and configured passwords. The cookie_generated
flag stays unset so wait_shutdown also skips deletion.

Refs: getfloresta#651
Replace Auth::UserPass with Auth::Hashed(Vec<RpcAuth>) and hash the
configured -rpcuser/-rpcpassword pair at startup. Florestad keeps only
(user, salt_hex, hash_hex) in memory; the plaintext password is
discarded after the HMAC.

RpcAuth::from_password rolls a 16-byte salt, hex-encodes it (32 ASCII
chars), and HMAC-SHA256s the password. RpcAuth::verify recomputes the
HMAC on each request and constant-time-compares both the username and
the digest.

The HMAC key is the salt's hex string as ASCII bytes (NOT the 16
decoded bytes), matching Bitcoin Core's share/rpcauth/rpcauth.py.
A pinned test vector asserts the digest matches rpcauth.py for a
known (salt, password) pair.

Refs: getfloresta#651
@Micah-Shallom
Copy link
Copy Markdown
Contributor Author

Add RpcAuth::parse for the <user>:<salt>$<hash> line format from
Bitcoin Core's share/rpcauth/rpcauth.py.

Collapse Auth into one Vec<RpcAuth> matching Core's g_rpcauth model.
Cookie auth is now another hashed entry: generate_cookie returns
(user, token) and the cookie's token is hashed and pushed alongside
any -rpcuser/-rpcpassword and -rpcauth entries.

Config gains rpc_auth: Vec<String> via --rpc-auth (repeatable) and an
auth = [...] key under [rpc] in the TOML file.

Refs: getfloresta#651
On credentials mismatch the middleware logs at warn level with the
peer's IP and sleeps 250 ms before returning the 401. Missing or
malformed headers still get an instant 401.

Peer address is extracted via axum's ConnectInfo, threaded through
with into_make_service_with_connect_info::<SocketAddr>() on the
router.

Refs: getfloresta#651
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

RPC Changes something with our JSON-RPC interface

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

feat: add authentication to JSON-RPC

4 participants