diff --git a/lib/src/nip17.dart b/lib/src/nip17.dart index 00a745b..812fbdb 100644 --- a/lib/src/nip17.dart +++ b/lib/src/nip17.dart @@ -9,23 +9,8 @@ import 'nostr_event.dart'; /// Recomputes a signed event's id from its fields and checks the BIP-340 /// signature against its pubkey. Throws if either is inconsistent. void _verifySignedEvent(Map event) { - final id = nostrEventId( - publicKey: event['pubkey'] as String, - createdAt: event['created_at'] as int, - kind: event['kind'] as int, - tags: - (event['tags'] as List).map((t) => (t as List).cast()).toList(), - content: event['content'] as String, - ); - if (id != event['id']) { - throw ArgumentError('Event id does not match its contents'); - } - if (!bip340.verify( - event['pubkey'] as String, - id, - event['sig'] as String, - )) { - throw ArgumentError('Event signature is invalid'); + if (!verifyNostrEvent(event)) { + throw ArgumentError('Event id or signature is invalid'); } } diff --git a/lib/src/nostr_event.dart b/lib/src/nostr_event.dart index 6ff1b81..2f01b6f 100644 --- a/lib/src/nostr_event.dart +++ b/lib/src/nostr_event.dart @@ -4,6 +4,30 @@ import 'dart:math'; import 'package:bip340/bip340.dart' as bip340; import 'package:crypto/crypto.dart'; +/// Verifies a decoded Nostr event: recomputes its id from the canonical +/// serialization and checks the BIP-340 signature against its pubkey. Returns +/// false (rather than throwing) for a malformed map, a mismatched id, or a bad +/// signature, so callers can drop untrusted events fetched from a relay. +bool verifyNostrEvent(Map event) { + try { + final publicKey = event['pubkey'] as String; + final sig = event['sig'] as String; + final id = nostrEventId( + publicKey: publicKey, + createdAt: event['created_at'] as int, + kind: event['kind'] as int, + tags: (event['tags'] as List) + .map((t) => (t as List).cast()) + .toList(), + content: event['content'] as String, + ); + if (id != event['id']) return false; + return bip340.verify(publicKey, id, sig); + } catch (_) { + return false; + } +} + /// Computes the Nostr event id: the lowercase hex SHA-256 of the canonical /// serialization `[0, pubkey, created_at, kind, tags, content]`. Shared by /// [signNostrEvent] and by unsigned events such as NIP-59 rumors, which carry diff --git a/lib/swarm.dart b/lib/swarm.dart index 650ad00..d27b2ad 100644 --- a/lib/swarm.dart +++ b/lib/swarm.dart @@ -3,6 +3,7 @@ export 'src/relay_connection.dart'; export 'src/nostr_publish_actor.dart'; export 'src/nostr_fetch_actor.dart'; export 'src/nostr_signer_actor.dart'; +export 'src/nostr_event.dart' show verifyNostrEvent; export 'src/geo_actor.dart'; export 'src/file_upload_actor.dart'; export 'src/file_download_actor.dart'; diff --git a/test/nostr_event_test.dart b/test/nostr_event_test.dart new file mode 100644 index 0000000..9a3a2b3 --- /dev/null +++ b/test/nostr_event_test.dart @@ -0,0 +1,53 @@ +import 'dart:convert'; + +import 'package:swarm/swarm.dart'; +import 'package:test/test.dart'; + +void main() { + // Produces a genuinely-signed event so verifyNostrEvent has a real signature + // to check (signed with the throwaway key below). + const privKey = + '0000000000000000000000000000000000000000000000000000000000000001'; + + Future> signedEvent() async { + final signer = NostrSignerActor(privKey); + final result = + await signer.handle( + const SignEvent(kind: 1985, content: '', tags: [ + ['L', 'app.xfay.moderation'], + ['l', 'hide', 'app.xfay.moderation'], + ['e', 'imageeventid'], + ]), + ) + as EventSigned; + return jsonDecode(result.event) as Map; + } + + test('accepts a genuinely signed event', () async { + expect(verifyNostrEvent(await signedEvent()), isTrue); + }); + + test('rejects a tampered content', () async { + final event = await signedEvent(); + event['content'] = 'tampered'; + expect(verifyNostrEvent(event), isFalse); + }); + + test('rejects a tampered id', () async { + final event = await signedEvent(); + event['id'] = '${event['id']}'.replaceRange(0, 1, '0'); + expect(verifyNostrEvent(event), isFalse); + }); + + test('rejects a forged author (pubkey swapped)', () async { + final event = await signedEvent(); + event['pubkey'] = + '0000000000000000000000000000000000000000000000000000000000000002'; + expect(verifyNostrEvent(event), isFalse); + }); + + test('returns false for a malformed map instead of throwing', () { + expect(verifyNostrEvent(const {}), isFalse); + expect(verifyNostrEvent(const {'kind': 1}), isFalse); + }); +}