feat(app): admin-signed group membership
Until now any member could change a group's roster or name. Groups now have admins, and every change to membership, roles or the name is an operation signed by an admin's identity key (XEdDSA). Recipients verify the signature against the admin's key from their own contacts, never a key carried in the same message; the relay plays no part. - Operations form a hash chain with an epoch; gaps or forks are held back and resolved by a signed state from an admin. - Groups created by older versions migrate with the creator as admin; if the creator has left, the smallest fingerprint takes over deterministically on every device. - Mixed groups with older clients keep working in a compatibility mode until every member advertises admin support; that capability can only be added, never withdrawn. - The last admin cannot step down or be removed. The verification rules live in a network-free policy module with its own tests.
This commit is contained in:
@@ -0,0 +1,427 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:sepot/core/crypto/group_admin.dart';
|
||||
import 'package:sepot/core/errors.dart';
|
||||
import 'package:sepot/core/services/group_admin_policy.dart';
|
||||
import 'package:sepot/models/group.dart';
|
||||
|
||||
/// Pravidla správců skupin bez sítě a bez nativní
|
||||
/// libsignal: migrace starých skupin, invarianty, ověření příchozí operace
|
||||
/// v pořadí 1–6 a drátová podoba `gop`. Podpis tu zastupuje fake — skutečný
|
||||
/// XEdDSA pokrývá group_admin_test.dart a groups_admin_flow_test.dart.
|
||||
void main() {
|
||||
const a = 'fpA'; // zakladatel, správce
|
||||
const b = 'fpB';
|
||||
const c = 'fpC';
|
||||
const d = 'fpD';
|
||||
final ikA = Uint8List.fromList(List.filled(33, 1));
|
||||
final goodSig = Uint8List.fromList([7, 7, 7]);
|
||||
|
||||
/// Fake podpis: platný je jen `goodSig` a jen s klíčem správce A.
|
||||
bool fakeVerify(GroupAdminOp op, Uint8List sig, Uint8List ik) =>
|
||||
_eq(sig, goodSig) && _eq(ik, ikA);
|
||||
|
||||
Group mkGroup({int epoch = 0, List<String>? admins, bool provisional = false}) =>
|
||||
Group(
|
||||
id: 'g1',
|
||||
name: 'Chata',
|
||||
memberIds: [a, b, c],
|
||||
createdBy: a,
|
||||
admins: admins,
|
||||
epoch: epoch,
|
||||
provisional: provisional,
|
||||
);
|
||||
|
||||
GroupOpMessage msgFor(
|
||||
Group local, {
|
||||
GroupAdminAction action = GroupAdminAction.add,
|
||||
String actor = a,
|
||||
int? epoch,
|
||||
List<String>? members,
|
||||
List<String>? admins,
|
||||
String? name,
|
||||
Uint8List? prev,
|
||||
Uint8List? sig,
|
||||
}) =>
|
||||
GroupOpMessage(
|
||||
op: GroupAdminOp(
|
||||
gid: local.id,
|
||||
epoch: epoch ?? local.epoch + 1,
|
||||
action: action,
|
||||
actor: actor,
|
||||
members: members ?? [...local.memberIds, d],
|
||||
admins: admins ?? local.admins,
|
||||
name: name ?? local.name,
|
||||
prev: prev ?? currentStateHash(local),
|
||||
),
|
||||
sig: sig ?? goodSig,
|
||||
);
|
||||
|
||||
GopVerdict check(Group local, GroupOpMessage msg,
|
||||
{String sender = a, Uint8List? ik}) =>
|
||||
checkGroupOp(
|
||||
local: local,
|
||||
msg: msg,
|
||||
senderFp: sender,
|
||||
me: b,
|
||||
actorIdentityKey: ik ?? ikA,
|
||||
verify: fakeVerify,
|
||||
);
|
||||
|
||||
group('migrace a model', () {
|
||||
test('skupina uložená bez správců: admins=[createdBy], epoch=0', () {
|
||||
final stara = {
|
||||
'id': 'g1',
|
||||
'name': 'Chata',
|
||||
'memberIds': [a, b, c],
|
||||
'createdBy': a,
|
||||
'createdAt': '2026-07-10T10:00:00.000',
|
||||
'left': false,
|
||||
};
|
||||
final g = Group.fromJson(stara);
|
||||
expect(g.admins, [a]);
|
||||
expect(g.epoch, 0);
|
||||
expect(g.v2Members, isEmpty);
|
||||
expect(g.pendingOps, isEmpty);
|
||||
expect(g.provisional, isTrue,
|
||||
reason: 'createdBy u v1 může být jen ten, kdo mě pozval');
|
||||
expect(g.isProvisionalFor(a), isFalse,
|
||||
reason: 'zakladatel si správce neodhaduje');
|
||||
expect(g.isProvisionalFor(b), isTrue);
|
||||
});
|
||||
|
||||
test('migrace skupiny, kterou zakladatel opustil: nastoupí nejmenší '
|
||||
'fingerprint (deterministicky u všech)', () {
|
||||
final g = Group.fromJson({
|
||||
'id': 'g1',
|
||||
'name': 'Chata',
|
||||
'memberIds': [c, b],
|
||||
'createdBy': a,
|
||||
'createdAt': '2026-07-10T10:00:00.000',
|
||||
});
|
||||
expect(g.admins, [b]);
|
||||
});
|
||||
|
||||
test('JSON round-trip zachová všechna v2 pole', () {
|
||||
final g = mkGroup(epoch: 3, admins: [a, b])
|
||||
..v2Members.addAll([b, c])
|
||||
..needsResync = true
|
||||
..rejectedOpAt = DateTime(2026, 9, 30, 12)
|
||||
..pendingOps = [
|
||||
PendingGroupOp(senderFp: a, wire: {'epoch': 5, 'sig': 'x'}),
|
||||
];
|
||||
final back = Group.fromJson(
|
||||
jsonDecode(jsonEncode(g.toJson())) as Map<String, dynamic>);
|
||||
expect(back.admins, [a, b]);
|
||||
expect(back.epoch, 3);
|
||||
expect(back.v2Members, {b, c});
|
||||
expect(back.needsResync, isTrue);
|
||||
expect(back.rejectedOpAt, DateTime(2026, 9, 30, 12));
|
||||
expect(back.pendingOps.single.senderFp, a);
|
||||
expect(back.pendingOps.single.epoch, 5);
|
||||
expect(back.provisional, isFalse);
|
||||
});
|
||||
|
||||
test('invariant: správci jsou podmnožina členů', () {
|
||||
final g = mkGroup(admins: [a, 'cizi']);
|
||||
expect(g.admins, [a]);
|
||||
g.memberIds = [b, c];
|
||||
g.normalizeAdmins();
|
||||
expect(g.admins, [b], reason: 'nikdy nezůstane bez správce');
|
||||
});
|
||||
});
|
||||
|
||||
group('kompatibilní režim', () {
|
||||
test('dokud je ve skupině v1 člen, skupina není vynucená', () {
|
||||
final g = mkGroup()..v2Members.addAll([a]);
|
||||
expect(g.isEnforced(b), isFalse, reason: 'c neinzeruje v2');
|
||||
g.v2Members.add(c);
|
||||
expect(g.isEnforced(b), isTrue,
|
||||
reason: 'všichni kromě mě inzerují v2 (já v2 jsem)');
|
||||
});
|
||||
});
|
||||
|
||||
group('ověření příchozí operace (pořadí 1–6)', () {
|
||||
test('poctivá navazující operace → apply', () {
|
||||
final g = mkGroup();
|
||||
expect(check(g, msgFor(g)), GopVerdict.apply);
|
||||
});
|
||||
|
||||
test('1. SPOOF: by ≠ sealed-sender autor', () {
|
||||
final g = mkGroup();
|
||||
expect(check(g, msgFor(g), sender: c), GopVerdict.spoof);
|
||||
});
|
||||
|
||||
test('2. NE-SPRÁVCE: podepsal člen bez role', () {
|
||||
final g = mkGroup();
|
||||
expect(check(g, msgFor(g, actor: c), sender: c), GopVerdict.notAdmin);
|
||||
});
|
||||
|
||||
test('3. ŠPATNÝ PODPIS i neznámý klíč podepisujícího', () {
|
||||
final g = mkGroup();
|
||||
expect(check(g, msgFor(g, sig: Uint8List.fromList([1]))),
|
||||
GopVerdict.badSignature);
|
||||
expect(check(g, msgFor(g), ik: Uint8List.fromList(List.filled(33, 9))),
|
||||
GopVerdict.badSignature);
|
||||
expect(
|
||||
checkGroupOp(
|
||||
local: g,
|
||||
msg: msgFor(g),
|
||||
senderFp: a,
|
||||
me: b,
|
||||
actorIdentityKey: null,
|
||||
verify: fakeVerify,
|
||||
),
|
||||
GopVerdict.badSignature);
|
||||
});
|
||||
|
||||
test('4a. MEZERA v epoše → gap', () {
|
||||
final g = mkGroup(epoch: 2);
|
||||
expect(check(g, msgFor(g, epoch: 4)), GopVerdict.gap);
|
||||
});
|
||||
|
||||
test('4b. ZASTARALÁ epocha (stejná i starší) → stale', () {
|
||||
final g = mkGroup(epoch: 2);
|
||||
expect(check(g, msgFor(g, epoch: 2)), GopVerdict.stale);
|
||||
expect(check(g, msgFor(g, epoch: 1)), GopVerdict.stale);
|
||||
});
|
||||
|
||||
test('5. FORK: prev nesedí s mým stavem', () {
|
||||
final g = mkGroup(epoch: 2);
|
||||
final jinaHistorie = hashState(
|
||||
gid: 'g1', epoch: 2, members: [a, b], admins: [a], name: 'Chata');
|
||||
expect(check(g, msgFor(g, prev: jinaHistorie)), GopVerdict.fork);
|
||||
});
|
||||
|
||||
test('invarianty příchozího stavu: prázdní správci, správce mimo členy, '
|
||||
'cizí skupina', () {
|
||||
final g = mkGroup();
|
||||
expect(check(g, msgFor(g, admins: [])), GopVerdict.invalid);
|
||||
expect(check(g, msgFor(g, members: [a, b], admins: [a, c])),
|
||||
GopVerdict.invalid);
|
||||
final cizi = GroupOpMessage(
|
||||
op: GroupAdminOp(
|
||||
gid: 'g2',
|
||||
epoch: 1,
|
||||
action: GroupAdminAction.add,
|
||||
actor: a,
|
||||
members: [a, b],
|
||||
admins: [a],
|
||||
name: 'X',
|
||||
prev: currentStateHash(g),
|
||||
),
|
||||
sig: goodSig,
|
||||
);
|
||||
expect(check(g, cizi), GopVerdict.invalid);
|
||||
});
|
||||
|
||||
test('sync od správce: jen ostře novější epocha', () {
|
||||
final g = mkGroup(epoch: 3);
|
||||
GroupOpMessage sync(int epoch) => msgFor(g,
|
||||
action: GroupAdminAction.sync, epoch: epoch, prev: Uint8List(0));
|
||||
expect(check(g, sync(5)), GopVerdict.apply);
|
||||
expect(check(g, sync(3)), GopVerdict.stale,
|
||||
reason: 'stejně stará větev nesmí přepínat stav tam a zpět');
|
||||
});
|
||||
|
||||
test('odhadnutý správce: první podepsaný stav od člena-správce je kotva',
|
||||
() {
|
||||
// B dostal pozvánku od C (v1), takže si myslí, že správce je C.
|
||||
final g = Group(
|
||||
id: 'g1',
|
||||
name: 'Chata',
|
||||
memberIds: [a, b, c],
|
||||
createdBy: c,
|
||||
provisional: true,
|
||||
);
|
||||
expect(g.admins, [c]);
|
||||
final kotva = msgFor(g, epoch: 7, prev: Uint8List(0), admins: [a]);
|
||||
expect(check(g, kotva), GopVerdict.apply,
|
||||
reason: 'prev ani epochu nejde ověřit, správce je v podpisu');
|
||||
expect(check(g, msgFor(g, actor: d, admins: [d]), sender: d),
|
||||
GopVerdict.notAdmin, reason: 'nečlen kotvu dát nesmí');
|
||||
});
|
||||
});
|
||||
|
||||
group('pozvánka s podpisem (join)', () {
|
||||
GroupOpMessage join({List<String>? members, List<String>? admins}) =>
|
||||
GroupOpMessage(
|
||||
op: GroupAdminOp(
|
||||
gid: 'g9',
|
||||
epoch: 0,
|
||||
action: GroupAdminAction.add,
|
||||
actor: a,
|
||||
members: members ?? [a, b],
|
||||
admins: admins ?? [a],
|
||||
name: 'Nová',
|
||||
prev: Uint8List(0),
|
||||
),
|
||||
sig: goodSig,
|
||||
);
|
||||
|
||||
GopVerdict checkJoin(GroupOpMessage m, {String sender = a}) => checkJoinOp(
|
||||
msg: m,
|
||||
senderFp: sender,
|
||||
me: b,
|
||||
actorIdentityKey: ikA,
|
||||
verify: fakeVerify,
|
||||
);
|
||||
|
||||
test('platná pozvánka', () => expect(checkJoin(join()), GopVerdict.apply));
|
||||
test('bez mě → invalid',
|
||||
() => expect(checkJoin(join(members: [a, c])), GopVerdict.invalid));
|
||||
test('podepsal ne-správce → notAdmin',
|
||||
() => expect(checkJoin(join(admins: [b])), GopVerdict.notAdmin));
|
||||
test('spoof → spoof',
|
||||
() => expect(checkJoin(join(), sender: c), GopVerdict.spoof));
|
||||
});
|
||||
|
||||
group('tvorba operace (odesílatel)', () {
|
||||
test('epocha +1, prev = hash mého stavu', () {
|
||||
final g = mkGroup(epoch: 4);
|
||||
final op = buildGroupOp(
|
||||
group: g, me: a, action: GroupAdminAction.rename, name: 'Jinak');
|
||||
expect(op.epoch, 5);
|
||||
expect(op.prev, currentStateHash(g));
|
||||
expect(op.name, 'Jinak');
|
||||
});
|
||||
|
||||
test('ne-správce operaci nepostaví', () {
|
||||
expect(
|
||||
() => buildGroupOp(
|
||||
group: mkGroup(), me: b, action: GroupAdminAction.rename),
|
||||
throwsA(isA<NotGroupAdminError>()));
|
||||
});
|
||||
|
||||
test('poslední správce se nesmí degradovat ani odebrat', () {
|
||||
final g = mkGroup();
|
||||
expect(
|
||||
() => buildGroupOp(
|
||||
group: g, me: a, action: GroupAdminAction.demote, admins: []),
|
||||
throwsA(isA<LastGroupAdminError>()));
|
||||
expect(
|
||||
() => buildGroupOp(
|
||||
group: g,
|
||||
me: a,
|
||||
action: GroupAdminAction.remove,
|
||||
members: [b, c],
|
||||
admins: [],
|
||||
),
|
||||
throwsA(isA<LastGroupAdminError>()));
|
||||
});
|
||||
|
||||
test('správce se smí degradovat, když zůstane jiný', () {
|
||||
final op = buildGroupOp(
|
||||
group: mkGroup(admins: [a, b]),
|
||||
me: a,
|
||||
action: GroupAdminAction.demote,
|
||||
admins: [b],
|
||||
);
|
||||
expect(op.admins, [b]);
|
||||
});
|
||||
|
||||
test('sync nečísluje epochu ani neřetězí', () {
|
||||
final op = buildGroupOp(
|
||||
group: mkGroup(epoch: 6), me: a, action: GroupAdminAction.sync);
|
||||
expect(op.epoch, 6);
|
||||
expect(op.prev, isEmpty);
|
||||
});
|
||||
|
||||
test('aplikace výsledku bez mě = odebrán', () {
|
||||
final g = mkGroup();
|
||||
applyGroupOp(
|
||||
g,
|
||||
buildGroupOp(
|
||||
group: g,
|
||||
me: a,
|
||||
action: GroupAdminAction.remove,
|
||||
members: [a, c],
|
||||
),
|
||||
b,
|
||||
);
|
||||
expect(g.left, isTrue);
|
||||
expect(g.epoch, 1);
|
||||
});
|
||||
});
|
||||
|
||||
group('drátová podoba gop', () {
|
||||
test('round-trip toWire → JSON → fromWire', () {
|
||||
final g = mkGroup(epoch: 2);
|
||||
final msg = GroupOpMessage(
|
||||
op: buildGroupOp(
|
||||
group: g,
|
||||
me: a,
|
||||
action: GroupAdminAction.promote,
|
||||
admins: [a, c],
|
||||
),
|
||||
sig: goodSig,
|
||||
roster: [
|
||||
{'f': c, 'n': 'Cyril', 'ik': 'AA==', 'v': 2},
|
||||
],
|
||||
);
|
||||
final wire = jsonDecode(jsonEncode(msg.toWire()));
|
||||
expect(wire['op'], 'promote');
|
||||
expect(wire['by'], a);
|
||||
final back = GroupOpMessage.fromWire(wire)!;
|
||||
expect(back.op.canonicalBytes(), msg.op.canonicalBytes(),
|
||||
reason: 'po cestě drátem se musí podepsané bajty shodovat');
|
||||
expect(back.sig, goodSig);
|
||||
expect(back.roster!.single['n'], 'Cyril');
|
||||
});
|
||||
|
||||
test('poškozená podoba → null, ne výjimka', () {
|
||||
expect(GroupOpMessage.fromWire(null), isNull);
|
||||
expect(GroupOpMessage.fromWire({'op': 'add'}), isNull);
|
||||
expect(
|
||||
GroupOpMessage.fromWire({
|
||||
'gid': 'g1',
|
||||
'epoch': 1,
|
||||
'op': 'nova-operace-z-budoucna',
|
||||
'by': a,
|
||||
'members': [a],
|
||||
'admins': [a],
|
||||
'gname': 'X',
|
||||
'prev': '',
|
||||
'sig': '',
|
||||
}),
|
||||
isNull);
|
||||
expect(
|
||||
GroupOpMessage.fromWire({
|
||||
'gid': 'g1',
|
||||
'epoch': 1,
|
||||
'op': 'add',
|
||||
'by': a,
|
||||
'members': [a],
|
||||
'admins': [a],
|
||||
'gname': 'X',
|
||||
'prev': '%%%',
|
||||
'sig': '',
|
||||
}),
|
||||
isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('podržené operace', () {
|
||||
test('seřazené podle epochy, bez duplicit, přetečení zahodí nejvzdálenější',
|
||||
() {
|
||||
final g = mkGroup();
|
||||
for (var e = Group.maxPendingOps + 5; e >= 2; e--) {
|
||||
holdGroupOp(g, PendingGroupOp(senderFp: a, wire: {'epoch': e, 'sig': '$e'}));
|
||||
}
|
||||
holdGroupOp(g, PendingGroupOp(senderFp: a, wire: {'epoch': 2, 'sig': '2'}));
|
||||
expect(g.pendingOps.length, Group.maxPendingOps);
|
||||
expect(g.pendingOps.first.epoch, 2);
|
||||
expect(g.pendingOps.last.epoch, Group.maxPendingOps + 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
bool _eq(List<int> x, List<int> y) {
|
||||
if (x.length != y.length) return false;
|
||||
for (var i = 0; i < x.length; i++) {
|
||||
if (x[i] != y[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:libsignal/libsignal.dart' show PrivateKey;
|
||||
import 'package:sepot/core/crypto/group_admin.dart';
|
||||
import 'package:sepot/core/crypto/sepot_crypto.dart';
|
||||
|
||||
/// Krypto jádro admin-signed skupin. Testuje kanonickou
|
||||
/// serializaci (determinismus + nezáměnnost polí) a XEdDSA sign/verify.
|
||||
///
|
||||
/// Serializační testy jsou pure-Dart a běží všude. Sign/verify potřebuje
|
||||
/// nativní libsignal (`liblibsignal_frb.so`) — v host `flutter test` bez ní
|
||||
/// se skupina PŘESKOČÍ (stejně jako ostatní krypto testy; ostré ověření běží
|
||||
/// na zařízení/CI s nativní knihovnou).
|
||||
void main() {
|
||||
GroupAdminOp op({
|
||||
String gid = 'g-1',
|
||||
int epoch = 1,
|
||||
GroupAdminAction action = GroupAdminAction.add,
|
||||
String actor = 'fpA',
|
||||
List<String>? members,
|
||||
List<String>? admins,
|
||||
String name = 'Parta',
|
||||
Uint8List? prev,
|
||||
}) =>
|
||||
GroupAdminOp(
|
||||
gid: gid,
|
||||
epoch: epoch,
|
||||
action: action,
|
||||
actor: actor,
|
||||
members: members ?? ['fpB', 'fpA', 'fpC'],
|
||||
admins: admins ?? ['fpA'],
|
||||
name: name,
|
||||
prev: prev ?? Uint8List(0),
|
||||
);
|
||||
|
||||
group('kanonická serializace', () {
|
||||
test('je deterministická a nezávislá na pořadí členů', () {
|
||||
final a = op(members: ['fpB', 'fpA', 'fpC']).canonicalBytes();
|
||||
final b = op(members: ['fpC', 'fpB', 'fpA']).canonicalBytes();
|
||||
expect(a, b, reason: 'setřídění rosteru → stejné bajty');
|
||||
});
|
||||
|
||||
test('duplicitní člen roster neovlivní', () {
|
||||
final a = op(members: ['fpA', 'fpB', 'fpC']).canonicalBytes();
|
||||
final b = op(members: ['fpA', 'fpB', 'fpB', 'fpC']).canonicalBytes();
|
||||
expect(a, b);
|
||||
});
|
||||
|
||||
test('přesun bajtů mezi poli změní podepisované bajty (délkový prefix)', () {
|
||||
// actor|name = "ab"|"c" vs "a"|"bc" — bez prefixu by konkatenace splynula
|
||||
final a = op(actor: 'ab', name: 'c').canonicalBytes();
|
||||
final b = op(actor: 'a', name: 'bc').canonicalBytes();
|
||||
expect(a, isNot(b));
|
||||
});
|
||||
|
||||
test('typ operace je součást podpisu', () {
|
||||
final a = op(action: GroupAdminAction.add).canonicalBytes();
|
||||
final b = op(action: GroupAdminAction.remove).canonicalBytes();
|
||||
expect(a, isNot(b));
|
||||
});
|
||||
|
||||
test('epocha, gid, admini a jméno mění bajty', () {
|
||||
final base = op().canonicalBytes();
|
||||
expect(op(epoch: 2).canonicalBytes(), isNot(base));
|
||||
expect(op(gid: 'g-2').canonicalBytes(), isNot(base));
|
||||
expect(op(admins: ['fpA', 'fpB']).canonicalBytes(), isNot(base));
|
||||
expect(op(name: 'Jiná').canonicalBytes(), isNot(base));
|
||||
});
|
||||
});
|
||||
|
||||
group('stateHash řetěz', () {
|
||||
test('různý stav → různý hash, stejný stav → stejný hash', () {
|
||||
final h1 = op(epoch: 1).stateHash();
|
||||
final h1b = op(epoch: 1).stateHash();
|
||||
final h2 = op(epoch: 2).stateHash();
|
||||
expect(h1, h1b);
|
||||
expect(h1, isNot(h2));
|
||||
expect(h1.length, 32);
|
||||
});
|
||||
});
|
||||
|
||||
group('XEdDSA sign/verify', () {
|
||||
var signalOk = false;
|
||||
setUpAll(() async {
|
||||
try {
|
||||
await SepotCrypto.init();
|
||||
signalOk = true;
|
||||
} catch (_) {
|
||||
signalOk = false; // nativní libsignal nedostupná (host bez .so)
|
||||
}
|
||||
});
|
||||
|
||||
bool ensureSignal() {
|
||||
if (!signalOk) {
|
||||
markTestSkipped('nativní libsignal nedostupná v host testu');
|
||||
}
|
||||
return signalOk;
|
||||
}
|
||||
|
||||
test('podpis admina se ověří jeho identity klíčem', () {
|
||||
if (!ensureSignal()) return;
|
||||
final priv = PrivateKey.generate();
|
||||
final pub = Uint8List.fromList(priv.getPublicKey().serialize());
|
||||
final o = op();
|
||||
final sig = o.signWith((m) => priv.sign(message: m));
|
||||
expect(o.verify(sig, pub), isTrue);
|
||||
});
|
||||
|
||||
test('pozměněná operace se stejným podpisem NEprojde', () {
|
||||
if (!ensureSignal()) return;
|
||||
final priv = PrivateKey.generate();
|
||||
final pub = Uint8List.fromList(priv.getPublicKey().serialize());
|
||||
final sig = op(name: 'Parta').signWith((m) => priv.sign(message: m));
|
||||
// Útočník přepíše jméno, ale podpis nechá starý.
|
||||
final tampered = op(name: 'Ovládnuto');
|
||||
expect(tampered.verify(sig, pub), isFalse);
|
||||
});
|
||||
|
||||
test('podpis jiného klíče neprojde (cizí admin)', () {
|
||||
if (!ensureSignal()) return;
|
||||
final signer = PrivateKey.generate();
|
||||
final impostor = Uint8List.fromList(
|
||||
PrivateKey.generate().getPublicKey().serialize(),
|
||||
);
|
||||
final o = op();
|
||||
final sig = o.signWith((m) => signer.sign(message: m));
|
||||
expect(o.verify(sig, impostor), isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user