feat: announce which key a room signs with, instead of rederiving it
A signer holds a different secret share under every ceremony it took part in, and signing with the wrong one produces a partial signature that cannot aggregate. Nothing said which was which: FrostSigningManager found a room's key by walking every ceremony this device holds a share for and rederiving each one's room id until one matched. That search can only find rooms derived at the one path the constant names. SharedKeyDerivation.parsePath was written to lift that limit and was never called, so a room derived anywhere else was invisible to signing. So the coordinator now says it. GroupKeyStateEvent (kind 30326) carries the threshold public key, the ceremony that made it and the path the room's id came from, posted into the room as its first application message and filed as a GroupKeyState row. completedKey reads that row first and follows it to the share. Nothing secret travels. Every member of the room can read the event, so a share on it would be each member holding everyone else's -- a 1-of-n key wearing a t-of-n's clothes. The event names the ceremony; the share stays in DkgSession.secretShare on the device that generated it. The coordinator is untrusted, as everywhere else in the ceremony, so a state is verified rather than believed: the room's id *is* the threshold key derived at the path, and one that does not rederive its own room is dropped. That is the same guarantee the rederivation gave, kept rather than traded for a lookup. The old scan stays behind it for rooms that predate the table. Announced after the members are added, which is the only order that works -- adding them commits a new epoch and MLS will not let a member read what was encrypted before the one they joined at. A member invited later still misses it and falls back to the scan, which is where every member was before this existed. Replacement is this app's job. These are rumors inside a Marmot group event, so no relay applies the 3xxxx rule, and the DAO keeps the newest announcement per room so a backfill cannot walk a room backwards. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0319f1613b
commit
a909108300
16 files changed
+6288
-21
No files matched your search
@@ -20,6 +20,7 @@ import press.mantra.compose.database.dao.FrostSigningSessionDao
|
||||
import press.mantra.compose.database.dao.GiftWrapMessageDao
|
||||
import press.mantra.compose.database.dao.GiftWrapPayloadDao
|
||||
import press.mantra.compose.database.dao.GiftWrapSealDao
|
||||
import press.mantra.compose.database.dao.GroupKeyStateDao
|
||||
import press.mantra.compose.database.dao.InReplyToRelationDao
|
||||
import press.mantra.compose.database.dao.MantraArtifactDao
|
||||
import press.mantra.compose.database.dao.MantraArtifactVersionDao
|
||||
@@ -71,6 +72,7 @@ import press.mantra.compose.database.model.Connection
|
||||
import press.mantra.compose.database.model.GiftWrapMessage
|
||||
import press.mantra.compose.database.model.GiftWrapPayload
|
||||
import press.mantra.compose.database.model.GiftWrapSeal
|
||||
import press.mantra.compose.database.model.GroupKeyState
|
||||
import press.mantra.compose.database.model.InReplyToRelation
|
||||
import press.mantra.compose.database.model.MantraArtifact
|
||||
import press.mantra.compose.database.model.MantraArtifactVersion
|
||||
@@ -132,6 +134,7 @@ val GENESIS_AT = Instant.fromEpochMilliseconds(1231006505000L)
|
||||
GiftWrapMessage::class,
|
||||
GiftWrapSeal::class,
|
||||
GiftWrapPayload::class,
|
||||
GroupKeyState::class,
|
||||
InReplyToRelation::class,
|
||||
MantraArtifact::class,
|
||||
MantraArtifactVersion::class,
|
||||
@@ -169,7 +172,7 @@ val GENESIS_AT = Instant.fromEpochMilliseconds(1231006505000L)
|
||||
UnsignedNostrEvent::class,
|
||||
Zap::class
|
||||
],
|
||||
version = 6,
|
||||
version = 7,
|
||||
autoMigrations = [
|
||||
// v2 only adds the DkgSession/DkgParticipantMessage tables, so Room can
|
||||
// generate the migration itself — nothing existing changes shape.
|
||||
@@ -194,7 +197,13 @@ val GENESIS_AT = Instant.fromEpochMilliseconds(1231006505000L)
|
||||
// both shapes Room can migrate itself. A ceremony that completed before
|
||||
// this reads back null, and signing falls back to not cross-checking
|
||||
// shares rather than refusing to run.
|
||||
AutoMigration(from = 5, to = 6)
|
||||
AutoMigration(from = 5, to = 6),
|
||||
// v7 adds the GroupKeyState table, which records what shared key a room
|
||||
// signs with instead of leaving it to be rederived. A new table is a
|
||||
// shape Room migrates itself. Rooms created before this have no row and
|
||||
// fall back to the rederivation scan in FrostSigningManager.completedKey,
|
||||
// which is why that scan stays.
|
||||
AutoMigration(from = 6, to = 7)
|
||||
]
|
||||
)
|
||||
@ColumnTypeConverters(MantraConverters::class)
|
||||
@@ -216,6 +225,8 @@ abstract class MantraDatabase: RoomDatabase() {
|
||||
|
||||
abstract fun frostSigningSessionDao(): FrostSigningSessionDao
|
||||
|
||||
abstract fun groupKeyStateDao(): GroupKeyStateDao
|
||||
|
||||
abstract fun connectionDao(): ConnectionDao
|
||||
|
||||
abstract fun giftWrapMessageDao(): GiftWrapMessageDao
|
||||
|
||||
Reference in new issue
Block a user