From fefbb243f002c951d95dcbf0dffe8ad7bfc6a9a1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Apr 2026 04:03:55 +0000 Subject: [PATCH] fix: correct GLV MINUS_LAMBDA constant and fe_cmp normalization bug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two critical bugs fixed: 1. GLV MINUS_LAMBDA d[1] and d[2] were wrong (0xC8B936E903BCBCBE vs correct 0xA880B9FC8EC739C2, and 0x5AD9E3FD77ED9BA3 vs correct 0x5AD9E3FD77ED9BA4). This caused the GLV scalar decomposition to produce wrong k1 values for all large scalars, making ecmult give wrong results. Verified by checking lambda^3 mod n == 1. 2. fe_cmp normalized both inputs before comparing, which reduced P itself to 0 (since P is in the range [P, 2^256) that normalize handles). This caused the "r < p" check in verify to fail for ALL valid signatures. Fixed by comparing raw limb values. Sign + verify + verify_fast now work correctly for small keys (1-3). Some keys with larger nonces still fail in the ecmult path — likely one more GLV/wNAF edge case remaining. https://claude.ai/code/session_011KVZhDcV2G7idNWEBz12GY --- .../marmot/MarmotSubscriptionManagerTest.kt | 32 ++++++++++--------- .../quartz/marmot/mls/MlsGroupEdgeCaseTest.kt | 4 +-- .../marmot/mls/MlsGroupLifecycleTest.kt | 18 +++++------ .../quartz/marmot/mls/MlsGroupTest.kt | 2 +- quartz/src/main/c/secp256k1/field.c | 9 +++--- quartz/src/main/c/secp256k1/scalar.c | 4 +-- 6 files changed, 36 insertions(+), 33 deletions(-) diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/marmot/MarmotSubscriptionManagerTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/marmot/MarmotSubscriptionManagerTest.kt index fd0984065..4842d831f 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/marmot/MarmotSubscriptionManagerTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/marmot/MarmotSubscriptionManagerTest.kt @@ -105,15 +105,16 @@ class MarmotSubscriptionManagerTest { } @Test - fun testGiftWrapFilter() { - val manager = MarmotSubscriptionManager(userPubKey) - val filter = manager.giftWrapFilter() + fun testGiftWrapFilter() = + runTest { + val manager = MarmotSubscriptionManager(userPubKey) + val filter = manager.giftWrapFilter() - assertEquals(listOf(GiftWrapEvent.KIND), filter.kinds) - assertNotNull(filter.tags) - assertEquals(listOf(userPubKey), filter.tags["p"]) - assertNull(filter.since) - } + assertEquals(listOf(GiftWrapEvent.KIND), filter.kinds) + assertNotNull(filter.tags) + assertEquals(listOf(userPubKey), filter.tags["p"]) + assertNull(filter.since) + } @Test fun testGiftWrapFilterWithSince() = @@ -154,14 +155,15 @@ class MarmotSubscriptionManagerTest { } @Test - fun testBuildFiltersWithNoGroupsHasGiftWrapAndKeyPackage() { - val manager = MarmotSubscriptionManager(userPubKey) - val allFilters = manager.buildFilters() + fun testBuildFiltersWithNoGroupsHasGiftWrapAndKeyPackage() = + runTest { + val manager = MarmotSubscriptionManager(userPubKey) + val allFilters = manager.buildFilters() - // Gift wrap filter + own key package filter - assertEquals(2, allFilters.size) - assertEquals(listOf(GiftWrapEvent.KIND), allFilters[0].kinds) - } + // Gift wrap filter + own key package filter + assertEquals(2, allFilters.size) + assertEquals(listOf(GiftWrapEvent.KIND), allFilters[0].kinds) + } @Test fun testKeyPackageFilter() { diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/marmot/mls/MlsGroupEdgeCaseTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/marmot/mls/MlsGroupEdgeCaseTest.kt index 62bf030a8..4647c3960 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/marmot/mls/MlsGroupEdgeCaseTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/marmot/mls/MlsGroupEdgeCaseTest.kt @@ -239,7 +239,7 @@ class MlsGroupEdgeCaseTest { // Advance through several epochs with empty commits for (i in 0 until 5) { val commitResult = alice.commit() - bob.processCommit(commitResult.commitBytes, alice.leafIndex) + bob.processCommit(commitResult.commitBytes, alice.leafIndex, ByteArray(0)) } assertEquals(7L, alice.epoch) // epoch 0 + addMember(1) + 5 commits = 6... wait @@ -277,7 +277,7 @@ class MlsGroupEdgeCaseTest { for (i in 0 until 3) { val commitResult = alice.commit() - bob.processCommit(commitResult.commitBytes, alice.leafIndex) + bob.processCommit(commitResult.commitBytes, alice.leafIndex, ByteArray(0)) keys.add(alice.exporterSecret("marmot", "group-event".encodeToByteArray(), 32)) } diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/marmot/mls/MlsGroupLifecycleTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/marmot/mls/MlsGroupLifecycleTest.kt index 7f5acd002..02806fa3e 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/marmot/mls/MlsGroupLifecycleTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/marmot/mls/MlsGroupLifecycleTest.kt @@ -192,7 +192,7 @@ class MlsGroupLifecycleTest { // Alice adds Carol (Bob processes Alice's commit) val carolBundle = createStandaloneKeyPackage("carol") val addCarolResult = alice.addMember(carolBundle.keyPackage.toTlsBytes()) - bob.processCommit(addCarolResult.commitBytes, alice.leafIndex) + bob.processCommit(addCarolResult.commitBytes, alice.leafIndex, ByteArray(0)) val carol = MlsGroup.processWelcome(addCarolResult.welcomeBytes!!, carolBundle) assertEquals(2L, alice.epoch) @@ -230,7 +230,7 @@ class MlsGroupLifecycleTest { val addCarolResult = bob.addMember(carolBundle.keyPackage.toTlsBytes()) // Alice processes Bob's commit - alice.processCommit(addCarolResult.commitBytes, bob.leafIndex) + alice.processCommit(addCarolResult.commitBytes, bob.leafIndex, ByteArray(0)) assertEquals(2L, alice.epoch) assertEquals(2L, bob.epoch) @@ -254,7 +254,7 @@ class MlsGroupLifecycleTest { assertEquals(1L, zara.epoch) // Alice processes the external commit - alice.processCommit(commitBytes, zara.leafIndex) + alice.processCommit(commitBytes, zara.leafIndex, ByteArray(0)) assertEquals(1L, alice.epoch) assertEquals(2, alice.memberCount) @@ -273,7 +273,7 @@ class MlsGroupLifecycleTest { val groupInfoBytes = alice.groupInfo().toTlsBytes() val (zara, commitBytes) = MlsGroup.externalJoin(groupInfoBytes, "zara".encodeToByteArray()) - alice.processCommit(commitBytes, zara.leafIndex) + alice.processCommit(commitBytes, zara.leafIndex, ByteArray(0)) val aliceKey = alice.exporterSecret("marmot", "group-event".encodeToByteArray(), 32) val zaraKey = zara.exporterSecret("marmot", "group-event".encodeToByteArray(), 32) @@ -327,7 +327,7 @@ class MlsGroupLifecycleTest { val commitResult = alice.commit() // Bob processes Alice's rotation commit - bob.processCommit(commitResult.commitBytes, alice.leafIndex) + bob.processCommit(commitResult.commitBytes, alice.leafIndex, ByteArray(0)) assertEquals(2L, alice.epoch) assertEquals(2L, bob.epoch) @@ -353,7 +353,7 @@ class MlsGroupLifecycleTest { // Alice rotates her signing key alice.proposeSigningKeyRotation() val commitResult = alice.commit() - bob.processCommit(commitResult.commitBytes, alice.leafIndex) + bob.processCommit(commitResult.commitBytes, alice.leafIndex, ByteArray(0)) // Both directions should still work after rotation val msg1 = "After rotation from Alice".encodeToByteArray() @@ -433,7 +433,7 @@ class MlsGroupLifecycleTest { val commitResult = alice.commit() // Bob processes the commit - bob.processCommit(commitResult.commitBytes, alice.leafIndex) + bob.processCommit(commitResult.commitBytes, alice.leafIndex, ByteArray(0)) assertEquals(epochBefore + 1, alice.epoch) assertEquals(alice.epoch, bob.epoch) @@ -464,7 +464,7 @@ class MlsGroupLifecycleTest { assertNotNull(alice.reInitPending, "ReInit should be pending after commit") // Bob processes and should also see reInit - bob.processCommit(commitResult.commitBytes, alice.leafIndex) + bob.processCommit(commitResult.commitBytes, alice.leafIndex, ByteArray(0)) assertNotNull(bob.reInitPending, "Bob should also see ReInit pending") } @@ -485,7 +485,7 @@ class MlsGroupLifecycleTest { // Alice commits with no proposals (purely for forward secrecy / UpdatePath) val commitResult = alice.commit() - bob.processCommit(commitResult.commitBytes, alice.leafIndex) + bob.processCommit(commitResult.commitBytes, alice.leafIndex, ByteArray(0)) assertEquals(epochBefore + 1, alice.epoch) assertEquals(alice.epoch, bob.epoch) diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/marmot/mls/MlsGroupTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/marmot/mls/MlsGroupTest.kt index 5c85c589b..81a0bf648 100644 --- a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/marmot/mls/MlsGroupTest.kt +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/marmot/mls/MlsGroupTest.kt @@ -264,7 +264,7 @@ class MlsGroupTest { assertEquals(1L, zara.epoch) // Alice processes Zara's external commit - alice.processCommit(commitBytes, zara.leafIndex) + alice.processCommit(commitBytes, zara.leafIndex, ByteArray(0)) assertEquals(1L, alice.epoch) assertEquals(2, alice.memberCount) } diff --git a/quartz/src/main/c/secp256k1/field.c b/quartz/src/main/c/secp256k1/field.c index 04abf014d..bd18aa941 100644 --- a/quartz/src/main/c/secp256k1/field.c +++ b/quartz/src/main/c/secp256k1/field.c @@ -276,11 +276,12 @@ int fe_from_bytes(secp256k1_fe *r, const uint8_t *in32) { } int fe_cmp(const secp256k1_fe *a, const secp256k1_fe *b) { - secp256k1_fe ta = *a, tb = *b; - fe_normalize_full(&ta); fe_normalize_full(&tb); + /* Compare raw limb values without normalization. + * fe_normalize reduces values in [P, 2^256) to [0, 2^32+977), + * which would turn P itself into 0 and break comparisons against P. */ for (int i = 3; i >= 0; i--) { - if (ta.d[i] < tb.d[i]) return -1; - if (ta.d[i] > tb.d[i]) return 1; + if (a->d[i] < b->d[i]) return -1; + if (a->d[i] > b->d[i]) return 1; } return 0; } diff --git a/quartz/src/main/c/secp256k1/scalar.c b/quartz/src/main/c/secp256k1/scalar.c index f49f0ec4f..7bd17fcf5 100644 --- a/quartz/src/main/c/secp256k1/scalar.c +++ b/quartz/src/main/c/secp256k1/scalar.c @@ -188,8 +188,8 @@ static const secp256k1_scalar GLV_MINUS_B2 = {{ }}; static const secp256k1_scalar GLV_MINUS_LAMBDA = {{ - 0xE0CFC810B51283CFULL, 0xC8B936E903BCBCBEULL, - 0x5AD9E3FD77ED9BA3ULL, 0xAC9C52B33FA3CF1FULL + 0xE0CFC810B51283CFULL, 0xA880B9FC8EC739C2ULL, + 0x5AD9E3FD77ED9BA4ULL, 0xAC9C52B33FA3CF1FULL }}; /*