Verify musig2 secret nonces (#108)

* Verify musig2 secret nonces

Trying to generate a musig2 partial signature with a secret nonce that was generated with a public key that does not match the signing key's public key will trigger secp256k1's illegal callback (which calls abort()) and crash the application.

=> Here we verify that the secret nonce matches the signing key before we call secp256k1_musig_partial_sign().
The verification method is a bit hackish (we extract the public key from the secret nonce blob) because secp256k1 does not export the methods we need to do this cleanly.
This commit is contained in:
Fabrice Drouin
2024-04-18 09:54:51 +02:00
committed by GitHub
parent eb92fccbd6
commit 567f411e12
4 changed files with 80 additions and 27 deletions

View File

@@ -5,6 +5,14 @@ import kotlin.test.*
class Secp256k1Test {
val random = Random.Default
fun randomBytes(length: Int): ByteArray {
val buffer = ByteArray(length)
random.nextBytes(buffer)
return buffer
}
@Test
fun verifyValidPrivateKey() {
val priv = Hex.decode("67E56582298859DDAE725F972992A07C6C4FB9F62A8FFF58CE3CA926A1063530".lowercase())
@@ -454,40 +462,55 @@ class Secp256k1Test {
@Test
fun testMusig2SigningSession() {
val privkeys = listOf(
"0101010101010101010101010101010101010101010101010101010101010101",
"0202020202020202020202020202020202020202020202020202020202020202",
).map { Hex.decode(it) }.toTypedArray()
val privkeys = listOf(randomBytes(32), randomBytes(32))
val sessionId = randomBytes(32)
val msg32 = randomBytes(32)
val pubkeys = privkeys.map { Secp256k1.pubkeyCreate(it) }
val sessionId = Hex.decode("0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F")
val nonces = pubkeys.map { Secp256k1.musigNonceGen(sessionId, null, it, null, null, null) }
val testData = run {
val builder = StringBuilder()
builder.append("private keys\n")
privkeys.forEach { builder.append(Hex.encode(it)).append("\n") }
builder.append("sessionId ${Hex.encode(sessionId)}\n")
builder.append("msg32 ${Hex.encode(msg32)}\n")
builder.append("nonces\n")
nonces.forEach { builder.append(Hex.encode(it)).append("\n") }
builder.toString()
}
val secnonces = nonces.map { it.copyOfRange(0, 132) }
val pubnonces = nonces.map { it.copyOfRange(132, 132 + 66) }
val aggnonce = Secp256k1.musigNonceAgg(pubnonces.toTypedArray())
val keyaggCaches = (0 until 2).map { ByteArray(Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) }
val aggpubkey = Secp256k1.musigPubkeyAgg(pubkeys.toTypedArray(), keyaggCaches[0])
assertContentEquals(aggpubkey, Secp256k1.musigPubkeyAgg(pubkeys.toTypedArray(), keyaggCaches[1]))
assertContentEquals(keyaggCaches[0], keyaggCaches[1])
assertContentEquals(aggpubkey, Secp256k1.musigPubkeyAgg(pubkeys.toTypedArray(), keyaggCaches[1]), testData)
assertContentEquals(keyaggCaches[0], keyaggCaches[1], testData)
val msg32 = Hex.decode("0303030303030303030303030303030303030303030303030303030303030303")
val sessions = (0 until 2).map { Secp256k1.musigNonceProcess(aggnonce, msg32, keyaggCaches[it]) }
val psigs = (0 until 2).map {
val psig = Secp256k1.musigPartialSign(secnonces[it], privkeys[it], keyaggCaches[it], sessions[it])
assertEquals(1, Secp256k1.musigPartialSigVerify(psig, pubnonces[it], pubkeys[it], keyaggCaches[it], sessions[it]))
assertEquals(0, Secp256k1.musigPartialSigVerify(Random.nextBytes(32), pubnonces[it], pubkeys[it], keyaggCaches[it], sessions[it]))
assertEquals(1, Secp256k1.musigPartialSigVerify(psig, pubnonces[it], pubkeys[it], keyaggCaches[it], sessions[it]), testData)
assertEquals(0, Secp256k1.musigPartialSigVerify(Random.nextBytes(32), pubnonces[it], pubkeys[it], keyaggCaches[it], sessions[it]), testData)
psig
}
// signing fails if the secret nonce does not match the private key's public key
assertFails(testData) {
Secp256k1.musigPartialSign(secnonces[1], privkeys[0], keyaggCaches[0], sessions[0])
}
assertFails(testData) {
Secp256k1.musigPartialSign(secnonces[0], privkeys[1], keyaggCaches[1], sessions[1])
}
val sig = Secp256k1.musigPartialSigAgg(sessions[0], psigs.toTypedArray())
assertContentEquals(sig, Secp256k1.musigPartialSigAgg(sessions[1], psigs.toTypedArray()))
assertTrue(Secp256k1.verifySchnorr(sig, msg32, aggpubkey))
assertContentEquals(sig, Secp256k1.musigPartialSigAgg(sessions[1], psigs.toTypedArray()), testData)
assertTrue(Secp256k1.verifySchnorr(sig, msg32, aggpubkey), testData)
val invalidSig1 = Secp256k1.musigPartialSigAgg(sessions[0], arrayOf(psigs[0], psigs[0]))
assertFalse(Secp256k1.verifySchnorr(invalidSig1, msg32, aggpubkey))
assertFalse(Secp256k1.verifySchnorr(invalidSig1, msg32, aggpubkey), testData)
val invalidSig2 = Secp256k1.musigPartialSigAgg(sessions[0], arrayOf(Random.nextBytes(32), Random.nextBytes(32)))
assertFalse(Secp256k1.verifySchnorr(invalidSig2, msg32, aggpubkey))
assertFalse(Secp256k1.verifySchnorr(invalidSig2, msg32, aggpubkey), testData)
}
@Test
@@ -523,15 +546,14 @@ class Secp256k1Test {
}
@Test
fun fuzzEcdsaSignVerify() {
val random = Random.Default
fun randomBytes(length: Int): ByteArray {
val buffer = ByteArray(length)
random.nextBytes(buffer)
return buffer
fun fuzzMusig2SigningSession() {
repeat(1000) {
testMusig2SigningSession()
}
}
@Test
fun fuzzEcdsaSignVerify() {
repeat(200) {
val priv = randomBytes(32)
assertTrue(Secp256k1.secKeyVerify(priv))