fix(indeehub): NIP-98 auth must prove an identity, never mint one
`sign_nip98` read the node's pubkey through `get_nostr_pubkey`, which goes
through `load_or_create_nostr_keys`. On a node with no Nostr identity that
does not fail — it GENERATES a keypair, writes the secret to disk, and signs
with it. So an IndeeHub auth header could quietly create a new node identity
as a side effect, then authenticate as a stranger holding a key nobody has
ever seen. The `.context("node has no Nostr identity")` guarding the call
could never fire, because the call could never fail that way.
`nostr_identity_exists` is the missing distinction: bootstrap may create,
but anything AUTHENTICATING as this node must prove the identity it already
has. sign_nip98 now gates on it and bails loudly.
Caught by `a_nip98_event_names_the_exact_url_and_method`, which asserts
exactly this ("must fail loudly rather than sign something empty") and had
been failing since the file landed in 58c759c1 — invisible because the
earlier runs on this branch filtered to `container::`.
Full bin suite 1381/1381.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
78f0370afc
commit
18f09d49d4
@@ -157,6 +157,16 @@ async fn nostr_session(client: &reqwest::Client, data_dir: &Path) -> Result<Stri
|
||||
/// a different endpoint, so they are set from the same values used to send.
|
||||
async fn sign_nip98(data_dir: &Path, url: &str, method: &str) -> Result<serde_json::Value> {
|
||||
let identity_dir = data_dir.join("identity");
|
||||
// Prove the identity this node already has; never mint one here.
|
||||
// `get_nostr_pubkey` goes through `load_or_create_nostr_keys`, so on a node
|
||||
// without an identity it would GENERATE a keypair, sign with it, and write
|
||||
// the secret to disk — an HTTP auth header quietly creating a new node
|
||||
// identity, and authenticating to IndeeHub as a stranger with a key nobody
|
||||
// has ever seen. The `.context("node has no Nostr identity")` below could
|
||||
// never fire because of it.
|
||||
if !nostr_discovery::nostr_identity_exists(&identity_dir).await {
|
||||
anyhow::bail!("node has no Nostr identity");
|
||||
}
|
||||
let pubkey = nostr_discovery::get_nostr_pubkey(&identity_dir)
|
||||
.await
|
||||
.context("node has no Nostr identity")?;
|
||||
|
||||
@@ -26,6 +26,20 @@ const D_TAG: &str = "archipelago-node";
|
||||
/// Relays we previously published to (for one-time revocation overwrite only)
|
||||
const LEGACY_RELAYS: &[&str] = &["wss://relay.damus.io", "wss://relay.nostr.info"];
|
||||
|
||||
/// Does this node already have a Nostr identity on disk?
|
||||
///
|
||||
/// `load_or_create_nostr_keys` mints one when it finds none. That is right at
|
||||
/// bootstrap and wrong for anything that AUTHENTICATES as this node: signing
|
||||
/// with a key generated on the spot proves nothing, presents the node as a
|
||||
/// stranger to whatever it is authenticating to, and writes a new secret to
|
||||
/// disk as a side effect of what the caller thought was a read. Callers that
|
||||
/// must prove an EXISTING identity gate on this first and fail loudly.
|
||||
pub(crate) async fn nostr_identity_exists(identity_dir: &Path) -> bool {
|
||||
fs::try_exists(identity_dir.join(NOSTR_SECRET_FILE))
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Load or create Nostr keys (secp256k1) for node discovery.
|
||||
pub(crate) async fn load_or_create_nostr_keys(identity_dir: &Path) -> Result<Keys> {
|
||||
let secret_path = identity_dir.join(NOSTR_SECRET_FILE);
|
||||
|
||||
Reference in New Issue
Block a user