Skip to content

Commit

Permalink
Verify musig2 secret nonces
Browse files Browse the repository at this point in the history
Trying to generate a musig2 partial signature with a secret nonce that was generated with a public key that does not match the
siging 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 siging keys 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.
  • Loading branch information
sstone committed Apr 16, 2024
1 parent eb92fcc commit e532ad8
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 14 deletions.
1 change: 1 addition & 0 deletions jni/src/main/kotlin/fr/acinq/secp256k1/NativeSecp256k1.kt
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ public object NativeSecp256k1 : Secp256k1 {
}

override fun musigPartialSign(secnonce: ByteArray, privkey: ByteArray, keyaggCache: ByteArray, session: ByteArray): ByteArray {
require(musigNoncevalidate(secnonce, pubkeyCreate(privkey)))
return Secp256k1CFunctions.secp256k1_musig_partial_sign(Secp256k1Context.getContext(), secnonce, privkey, keyaggCache, session)
}

Expand Down
21 changes: 21 additions & 0 deletions src/commonMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,26 @@ public interface Secp256k1 {
*/
public fun musigNonceProcess(aggnonce: ByteArray, msg32: ByteArray, keyaggCache: ByteArray): ByteArray

/**
* Validate a musig2 secret nonce
* @param secretnonce secret nonce
* @param pubkey public key that was passed to the nonce generation method
* @return false if the secret nonce does not match the public key
*/
public fun musigNoncevalidate(secretnonce: ByteArray, pubkey: ByteArray): Boolean {
if (secretnonce.size != MUSIG2_SECRET_NONCE_SIZE) return false
if (pubkey.size != 33 && pubkey.size != 65) return false
val pk = Secp256k1.pubkeyParse(pubkey)
// this is a bit hackish but the secp256k1 library does not export methods to do this cleanly
val x = secretnonce.copyOfRange(68, 68 + 32)
x.reverse()
val y = secretnonce.copyOfRange(68 + 32, 68 + 32 + 32)
y.reverse()
val pkx = pk.copyOfRange(1, 1 + 32)
val pky = pk.copyOfRange(33, 33 + 32)
return x.contentEquals(pkx) && y.contentEquals(pky)
}

/**
* Create a partial signature.
*
Expand Down Expand Up @@ -256,6 +276,7 @@ public interface Secp256k1 {
*/
public fun cleanup()


public companion object : Secp256k1 by getSecpk256k1() {
@JvmStatic
public fun get(): Secp256k1 = this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,7 @@ public object Secp256k1Native : Secp256k1 {
require(privkey.size == 32)
require(keyaggCache.size == Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE)
require(session.size == Secp256k1.MUSIG2_PUBLIC_SESSION_SIZE)
require(musigNoncevalidate(secnonce, pubkeyCreate(privkey)))

memScoped {
val nSecnonce = alloc<secp256k1_musig_secnonce>()
Expand Down
41 changes: 27 additions & 14 deletions tests/src/commonTest/kotlin/fr/acinq/secp256k1/Secp256k1Test.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -451,16 +459,13 @@ class Secp256k1Test {
val agg3 = Secp256k1.musigPubkeyXonlyTweakAdd(cache, Hex.decode("7468697320636f756c64206265206120746170726f6f7420747765616b2e2e00"))
assertEquals("04537a081a8d32ff700ca86aaa77a423e9b8d1480938076b645c68ee39d263c93948026928799b2d942cb5851db397015b26b1759de1b9ab2c691ced64a2eef836", Hex.encode(agg3))
}

@Test
fun testMusig2SigningSession() {
val privkeys = listOf(
"0101010101010101010101010101010101010101010101010101010101010101",
"0202020202020202020202020202020202020202020202020202020202020202",
).map { Hex.decode(it) }.toTypedArray()
val privkeys = listOf(randomBytes(32), randomBytes(32))
val pubkeys = privkeys.map { Secp256k1.pubkeyCreate(it) }

val sessionId = Hex.decode("0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F")
val sessionId = randomBytes(32)
val nonces = pubkeys.map { Secp256k1.musigNonceGen(sessionId, null, it, null, null, null) }
val secnonces = nonces.map { it.copyOfRange(0, 132) }
val pubnonces = nonces.map { it.copyOfRange(132, 132 + 66) }
Expand All @@ -471,7 +476,7 @@ class Secp256k1Test {
assertContentEquals(aggpubkey, Secp256k1.musigPubkeyAgg(pubkeys.toTypedArray(), keyaggCaches[1]))
assertContentEquals(keyaggCaches[0], keyaggCaches[1])

val msg32 = Hex.decode("0303030303030303030303030303030303030303030303030303030303030303")
val msg32 = randomBytes(32)
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])
Expand All @@ -480,6 +485,15 @@ class Secp256k1Test {
psig
}

// signing fails if the secret nonce does not match the private key's public key
assertFails {
Secp256k1.musigPartialSign(secnonces[1], privkeys[0], keyaggCaches[0], sessions[0])
}

assertFails {
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))
Expand Down Expand Up @@ -523,15 +537,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))
Expand Down

0 comments on commit e532ad8

Please sign in to comment.