fix(mobile): use only the verified envelope for a repeated inventory ID

The verification cache was keyed by event ID but then read the current
envelope, so a relay could resend a verified ID with a different d tag
and skip verification. A repeated ID now uses the envelope verified for
it, and a regression covers both orders of the forged copy.

Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
Tom Brow
2026-10-26 21:19:30 -07:00
parent 2073e26215
commit be269eb340
2 changed files with 43 additions and 8 deletions

View File

@@ -105,7 +105,7 @@ Future<List<String>> _managedAgentPubkeys(
String? agent,
}) async {
final heads = <String, NostrEvent>{};
final verified = <String>{};
final verified = <String, NostrEvent>{};
NostrEvent? cursor;
for (var page = 0; ; page++) {
if (page == _maxInventoryPages) {
@@ -135,21 +135,23 @@ Future<List<String>> _managedAgentPubkeys(
throw StateError('Agent inventory response is outside its owner.');
}
for (final event in events) {
final d = event.getTagValue('d');
if (d == null || (agent != null && d != agent)) continue;
// Like desktop, skip an envelope that fails verification so a forged
// record can neither add an agent nor supersede a genuine one. An ID
// already verified needs no second check when pages overlap.
if (!verified.contains(event.id)) {
// record can neither add an agent nor supersede a genuine one. When
// pages overlap, a repeated ID uses only the envelope already verified
// for it, never the bytes the relay sent again.
var record = verified[event.id];
if (record == null) {
try {
nostr.Event.fromJson(jsonEncode(event.toJson()));
} catch (_) {
continue;
}
verified.add(event.id);
record = verified[event.id] = event;
}
final d = record.getTagValue('d');
if (d == null || (agent != null && d != agent)) continue;
final previous = heads[d];
if (previous == null || _newer(event, previous)) heads[d] = event;
if (previous == null || _newer(record, previous)) heads[d] = record;
}
if (events.length < _pageSize) break;
// Relay order is newest first, then ID ascending at ties.

View File

@@ -452,6 +452,39 @@ void main() {
isFalse,
);
});
for (final forgedFirst in [false, true]) {
test(
'a repeated ID cannot inject an agent (forged first: $forgedFirst)',
() async {
final genuine = managed(agent.public);
final forged = NostrEvent.fromJson(
genuine.toJson()
..['tags'] = [
['d', other.public],
],
);
final relay = _Relay(
inventory: [
forgedFirst ? [forged, genuine] : [genuine, forged],
],
profiles: [
profile(agent, ownedBy: owner),
profile(other, ownedBy: owner),
],
);
final container = setup(relay);
expect(
(await container.read(
meAssistantsProvider.future,
)).map((a) => a.pubkey),
[agent.public],
);
final sync = container.read(meSyncProvider);
expect(await isOwnedMeAssistant(sync, other.public), isFalse);
expect(await isOwnedMeAssistant(sync, agent.public), isTrue);
},
);
}
test(
'creation rejects revoked ownership and unrelated returned profiles',
() async {