fix(relay): gate KIND_DREAM_DUE on matching #p to prevent info leak

KIND_DREAM_DUE (24300) was absent from P_GATED_KINDS, so
p_gated_filters_authorized() never enforced #p-must-match-self for it.
Any authenticated relay user could subscribe with kinds:[24300],
#p:[victim_pubkey] and receive another agent's dream-due signals,
leaking "agent X is over memory budget and idle" to anyone who knows
an agent pubkey.

The kind's own doc already states "single-delivery to the authenticated
agent"; P_GATED_KINDS is the registry that makes the relay enforce it.

Fix: add KIND_DREAM_DUE to P_GATED_KINDS. Ephemeral kinds are included
in this list for filter-layer enforcement only and are never stored, so
no schema/migration/tsvector change is needed.

The ACP harness's own subscription (DREAM_SIGNAL_SUB_ID, #p=[self])
continues to be accepted — the self case is exactly what P_GATED_KINDS
allows through.

Test: dream_due_subscription_requires_matching_p_tag verifies:
- no #p  →  rejected
- #p:[other]  →  rejected
- #p:[self]  →  accepted
Confirmed: test fails without the registry add and passes with it.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7
2026-06-30 14:47:51 -04:00
co-authored by Will Pfleger
parent ef963a3353
commit 30e25b4a41
2 changed files with 28 additions and 0 deletions
+1
View File
@@ -131,6 +131,7 @@ pub const P_GATED_KINDS: &[u32] = &[
KIND_MEMBER_REMOVED_NOTIFICATION,
KIND_GIFT_WRAP,
KIND_DM_VISIBILITY,
KIND_DREAM_DUE,
];
/// NIP-AP: Agent Persona (parameterized replaceable, owner-authored).
+27
View File
@@ -1296,6 +1296,33 @@ mod tests {
assert!(p_gated_filters_authorized(&[matching_p], authed));
}
#[test]
fn dream_due_subscription_requires_matching_p_tag() {
// KIND_DREAM_DUE (24300) is a #p-gated ephemeral kind: "single-delivery
// to the authenticated agent". A filter targeting another agent's pubkey
// must be rejected; a filter targeting self must be accepted.
let p_tag = SingleLetterTag::lowercase(Alphabet::P);
let authed = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
let other = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
// No #p at all → rejected (could subscribe to all agents' dream signals).
let no_p = Filter::new()
.kind(nostr::Kind::Custom(buzz_core::kind::KIND_DREAM_DUE as u16));
assert!(!p_gated_filters_authorized(&[no_p], authed));
// #p targeting another agent → rejected (info leak: "agent X is over budget + idle").
let wrong_p = Filter::new()
.kind(nostr::Kind::Custom(buzz_core::kind::KIND_DREAM_DUE as u16))
.custom_tags(p_tag, [other]);
assert!(!p_gated_filters_authorized(&[wrong_p], authed));
// #p targeting self → accepted (this is the ACP harness's own subscription).
let self_p = Filter::new()
.kind(nostr::Kind::Custom(buzz_core::kind::KIND_DREAM_DUE as u16))
.custom_tags(p_tag, [authed]);
assert!(p_gated_filters_authorized(&[self_p], authed));
}
#[test]
fn d_tag_pushdown_only_for_nip33_kinds() {
let d_tag = SingleLetterTag::lowercase(Alphabet::D);