feat(relay): add relay-verified identity binding - #1476
Conversation
bd822f3 to
7ba8616
Compare
b441953 to
c1452ef
Compare
419e851 to
7cd8b9e
Compare
There was a problem hiding this comment.
edit: Sorry, Codex posted this...
edit2: Ok, reviewed Codex's comments and I agree
The binding model and transport wiring are thoughtfully structured, and the current CI run is green. I found three security-boundary gaps that make BUZZ_REQUIRE_CORPORATE_IDENTITY weaker than its configuration contract: tokens without aud are accepted, HTTP bridge bindings can originate from the unverified X-Pubkey fallback, and existing authenticated read paths bypass the identity gate. Requesting changes until these are closed and covered by regression tests.
|
|
||
| let mut validation = Validation::new(header.alg); | ||
| validation.set_issuer(&[self.config.issuer.as_str()]); | ||
| validation.set_audience(&[self.config.audience.as_str()]); |
There was a problem hiding this comment.
[P1] Require the audience claim itself
In jsonwebtoken 9, Validation::new requires only exp. set_audience configures the allowed value, but validation is skipped when aud is absent. As written, a correctly signed issuer token with no audience can enroll and bind a principal even though it was not issued for Buzz. Add aud to required_spec_claims (and a missing-aud regression test) before decoding.
There was a problem hiding this comment.
🤖 Addressed on current head. JWT validation now explicitly requires exp, iss, and aud, and rejects missing or malformed audiences. Regression tests cover both cases.
| let pubkey_bytes = pubkey.to_bytes().to_vec(); | ||
|
|
||
| let auth_tag = headers.get("x-auth-tag").and_then(|v| v.to_str().ok()); | ||
| enforce_bridge_corporate_identity(state, tenant, headers, pubkey, auth_tag).await?; |
There was a problem hiding this comment.
[P1] Require cryptographic Nostr proof before persisting this binding
verify_bridge_auth still returns the caller-controlled X-Pubkey fallback whenever BUZZ_REQUIRE_AUTH_TOKEN=false, which is the default. This call then binds the validated JWT uid to that arbitrary key. A valid JWT holder can therefore bind another user pubkey to their own principal and leave the real owner hitting a persistent pubkey conflict. When corporate identity is enabled, force NIP-98 on these routes or reject the incompatible startup configuration.
There was a problem hiding this comment.
🤖 Addressed. Enabling corporate identity now forces signed NIP-98 authentication even when ordinary token auth is disabled, so the caller-controlled X-Pubkey fallback cannot create a durable binding. A regression test verifies this returns 401.
| headers: &HeaderMap, | ||
| pubkey: nostr::PublicKey, | ||
| auth_tag: Option<&str>, | ||
| ) -> Result<(), (StatusCode, Json<Value>)> { |
There was a problem hiding this comment.
[P1] Apply this gate to the other authenticated bridge paths
The new helper is called by /events, /query, and /count, but authorize_moderation_read in this same module still stops after NIP-98 replay and role authorization. With BUZZ_REQUIRE_CORPORATE_IDENTITY=true, a moderator without any identity JWT can still read reports, audit records, and restrictions. Route every community-scoped NIP-authenticated bridge entry point through the same post-proof identity check.
There was a problem hiding this comment.
🤖 Addressed. /moderation/reports, /moderation/audit, and /moderation/restricted now share the same corporate-identity gate after NIP-98 verification and before database reads. Binding finalization occurs only after moderator authorization succeeds.
| crate::corporate_identity::enforce_corporate_identity( | ||
| state, | ||
| tenant.community(), | ||
| auth_event.pubkey, |
There was a problem hiding this comment.
[P1] Gate authenticated media reads as well as uploads
This covers upload, but authenticate_media_read still verifies only the Blossom GET event and relay membership. When BUZZ_REQUIRE_MEDIA_GET_AUTH and BUZZ_REQUIRE_CORPORATE_IDENTITY are both true, a member key can continue GET/HEAD access to protected blobs without a JWT. Apply the same identity enforcement there using auth_event.pubkey before admitting the read.
There was a problem hiding this comment.
🤖 Addressed. Protected Blossom GET and HEAD requests now verify the Blossom proof, validate corporate identity, enforce membership, and only then finalize the binding before accessing storage. Public media remains an intentional documented exemption.
jmecom
left a comment
There was a problem hiding this comment.
Re-reviewed the current head. The four prior blockers are addressed: JWT audience is required, corporate mode requires signed NIP-98 proof on the HTTP bridge, and corporate identity is enforced on moderation and protected media reads.
I also reviewed the staged/finalized authorization flow, atomic invite enrollment, revocation and rotation handling, session eviction, route inventory, and expiring desktop trust projection. I found no additional blocking issues.
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com>
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com>
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com>
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com>
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com>
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com>
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com>
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com>
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com>
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com>
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com>
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com>
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com>
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com>
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com>
0390d06 to
a28957d
Compare
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com>
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com>
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com>
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com>
Why
Add a provider-neutral relay-side identity foundation that binds a verified JWT principal to a NIP-proven Nostr key.
What
(community, issuer, uid)bindings with fail-closed conflict handling and durable revocation tombstones.Risk Assessment
Medium. This touches every relay authentication transport, but the feature is disabled by default behind
BUZZ_REQUIRE_CORPORATE_IDENTITYand fails closed when enabled.Follow-ups
Notes
When enabled, the configured JWT acts as an enrollment authority: first use binds the verified issuer-qualified principal to the NIP-proven pubkey. A compromised valid JWT can therefore create an incorrect binding until an operator repairs it; automated recovery remains follow-up lifecycle work.
Trusted deployments must overwrite the configured identity header at the relay boundary. JWKS failures fail closed, and configured display-name claims may result in stored personal information.
Update — 2026-07-28
Update — 2026-07-29
Generated with Codex
Update — 2026-07-29 (security hardening)
This supersedes the earlier lifecycle follow-ups and first-use enrollment caveat.
jsonwebtoken, required and type-checkedexp/iss/aud, enforcednbf, asymmetric algorithms, JWKalg/use/key_ops, strict corporate config, mandatory configured npub, and bounded single-flight JWKS fetching.