verifyNostrEvent helper
This commit is contained in:
parent
f69fcb661f
commit
0e70aba755
4 changed files with 80 additions and 17 deletions
|
|
@ -9,23 +9,8 @@ import 'nostr_event.dart';
|
||||||
/// Recomputes a signed event's id from its fields and checks the BIP-340
|
/// Recomputes a signed event's id from its fields and checks the BIP-340
|
||||||
/// signature against its pubkey. Throws if either is inconsistent.
|
/// signature against its pubkey. Throws if either is inconsistent.
|
||||||
void _verifySignedEvent(Map<String, dynamic> event) {
|
void _verifySignedEvent(Map<String, dynamic> event) {
|
||||||
final id = nostrEventId(
|
if (!verifyNostrEvent(event)) {
|
||||||
publicKey: event['pubkey'] as String,
|
throw ArgumentError('Event id or signature is invalid');
|
||||||
createdAt: event['created_at'] as int,
|
|
||||||
kind: event['kind'] as int,
|
|
||||||
tags:
|
|
||||||
(event['tags'] as List).map((t) => (t as List).cast<String>()).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');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,30 @@ import 'dart:math';
|
||||||
import 'package:bip340/bip340.dart' as bip340;
|
import 'package:bip340/bip340.dart' as bip340;
|
||||||
import 'package:crypto/crypto.dart';
|
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<String, dynamic> 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<String>())
|
||||||
|
.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
|
/// Computes the Nostr event id: the lowercase hex SHA-256 of the canonical
|
||||||
/// serialization `[0, pubkey, created_at, kind, tags, content]`. Shared by
|
/// serialization `[0, pubkey, created_at, kind, tags, content]`. Shared by
|
||||||
/// [signNostrEvent] and by unsigned events such as NIP-59 rumors, which carry
|
/// [signNostrEvent] and by unsigned events such as NIP-59 rumors, which carry
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ export 'src/relay_connection.dart';
|
||||||
export 'src/nostr_publish_actor.dart';
|
export 'src/nostr_publish_actor.dart';
|
||||||
export 'src/nostr_fetch_actor.dart';
|
export 'src/nostr_fetch_actor.dart';
|
||||||
export 'src/nostr_signer_actor.dart';
|
export 'src/nostr_signer_actor.dart';
|
||||||
|
export 'src/nostr_event.dart' show verifyNostrEvent;
|
||||||
export 'src/geo_actor.dart';
|
export 'src/geo_actor.dart';
|
||||||
export 'src/file_upload_actor.dart';
|
export 'src/file_upload_actor.dart';
|
||||||
export 'src/file_download_actor.dart';
|
export 'src/file_download_actor.dart';
|
||||||
|
|
|
||||||
53
test/nostr_event_test.dart
Normal file
53
test/nostr_event_test.dart
Normal file
|
|
@ -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<Map<String, dynamic>> 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<String, dynamic>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue