Skip to content

Commit 567f411

Browse files
authored
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.
1 parent eb92fcc commit 567f411

File tree

4 files changed

+80
-27
lines changed

4 files changed

+80
-27
lines changed

jni/src/main/kotlin/fr/acinq/secp256k1/NativeSecp256k1.kt

+1
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ public object NativeSecp256k1 : Secp256k1 {
117117
}
118118

119119
override fun musigPartialSign(secnonce: ByteArray, privkey: ByteArray, keyaggCache: ByteArray, session: ByteArray): ByteArray {
120+
require(musigNonceValidate(secnonce, pubkeyCreate(privkey)))
120121
return Secp256k1CFunctions.secp256k1_musig_partial_sign(Secp256k1Context.getContext(), secnonce, privkey, keyaggCache, session)
121122
}
122123

src/commonMain/kotlin/fr/acinq/secp256k1/Secp256k1.kt

+20
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,26 @@ public interface Secp256k1 {
217217
*/
218218
public fun musigNonceProcess(aggnonce: ByteArray, msg32: ByteArray, keyaggCache: ByteArray): ByteArray
219219

220+
/**
221+
* Check that a secret nonce was generated with a public key that matches the private key used for signing.
222+
* @param secretnonce secret nonce.
223+
* @param pubkey public key for the private key that will be used, with the secret nonce, to generate a partial signature.
224+
* @return false if the secret nonce does not match the public key.
225+
*/
226+
public fun musigNonceValidate(secretnonce: ByteArray, pubkey: ByteArray): Boolean {
227+
if (secretnonce.size != MUSIG2_SECRET_NONCE_SIZE) return false
228+
if (pubkey.size != 33 && pubkey.size != 65) return false
229+
val pk = Secp256k1.pubkeyParse(pubkey)
230+
// this is a bit hackish but the secp256k1 library does not export methods to do this cleanly
231+
val x = secretnonce.copyOfRange(68, 68 + 32)
232+
x.reverse()
233+
val y = secretnonce.copyOfRange(68 + 32, 68 + 32 + 32)
234+
y.reverse()
235+
val pkx = pk.copyOfRange(1, 1 + 32)
236+
val pky = pk.copyOfRange(33, 33 + 32)
237+
return x.contentEquals(pkx) && y.contentEquals(pky)
238+
}
239+
220240
/**
221241
* Create a partial signature.
222242
*

src/nativeMain/kotlin/fr/acinq/secp256k1/Secp256k1Native.kt

+15-5
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ public object Secp256k1Native : Secp256k1 {
8181
return serialized.readBytes(Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE)
8282
}
8383

84-
private fun DeferScope.toNat(bytes: ByteArray): CPointer<UByteVar> {
84+
private fun DeferScope.toNat(bytes: ByteArray): CPointer<UByteVar> {
8585
val ubytes = bytes.asUByteArray()
8686
val pinned = ubytes.pin()
8787
this.defer { pinned.unpin() }
@@ -112,7 +112,7 @@ public object Secp256k1Native : Secp256k1 {
112112
}
113113

114114
public override fun signatureNormalize(sig: ByteArray): Pair<ByteArray, Boolean> {
115-
require(sig.size >= 64){ "invalid signature ${Hex.encode(sig)}" }
115+
require(sig.size >= 64) { "invalid signature ${Hex.encode(sig)}" }
116116
memScoped {
117117
val nSig = allocSignature(sig)
118118
val isHighS = secp256k1_ecdsa_signature_normalize(ctx, nSig.ptr, nSig.ptr)
@@ -307,7 +307,16 @@ public object Secp256k1Native : Secp256k1 {
307307
memcpy(n.ptr, toNat(it), Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE.toULong())
308308
n
309309
}
310-
secp256k1_musig_nonce_gen(ctx, secnonce.ptr, pubnonce.ptr, toNat(sessionId32), privkey?.let { toNat(it) }, nPubkey.ptr, msg32?.let { toNat(it) },nKeyAggCache?.ptr, extraInput32?.let { toNat(it) }).requireSuccess("secp256k1_musig_nonce_gen() failed")
310+
secp256k1_musig_nonce_gen(
311+
ctx,
312+
secnonce.ptr,
313+
pubnonce.ptr,
314+
toNat(sessionId32),
315+
privkey?.let { toNat(it) },
316+
nPubkey.ptr,
317+
msg32?.let { toNat(it) },
318+
nKeyAggCache?.ptr,
319+
extraInput32?.let { toNat(it) }).requireSuccess("secp256k1_musig_nonce_gen() failed")
311320
val nPubnonce = allocArray<UByteVar>(Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE)
312321
secp256k1_musig_pubnonce_serialize(ctx, nPubnonce, pubnonce.ptr).requireSuccess("secp256k1_musig_pubnonce_serialize failed")
313322
secnonce.ptr.readBytes(Secp256k1.MUSIG2_SECRET_NONCE_SIZE) + nPubnonce.readBytes(Secp256k1.MUSIG2_PUBLIC_NONCE_SIZE)
@@ -339,7 +348,7 @@ public object Secp256k1Native : Secp256k1 {
339348
n
340349
}
341350
secp256k1_musig_pubkey_agg(ctx, combined.ptr, nKeyAggCache?.ptr, nPubkeys.toCValues(), pubkeys.size.convert()).requireSuccess("secp256k1_musig_nonce_agg() failed")
342-
val agg = serializeXonlyPubkey(combined)
351+
val agg = serializeXonlyPubkey(combined)
343352
keyaggCache?.let { blob -> nKeyAggCache?.let { memcpy(toNat(blob), it.ptr, Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE.toULong()) } }
344353
return agg
345354
}
@@ -386,13 +395,14 @@ public object Secp256k1Native : Secp256k1 {
386395
memcpy(toNat(session), nSession.ptr, Secp256k1.MUSIG2_PUBLIC_SESSION_SIZE.toULong())
387396
return session
388397
}
389-
}
398+
}
390399

391400
override fun musigPartialSign(secnonce: ByteArray, privkey: ByteArray, keyaggCache: ByteArray, session: ByteArray): ByteArray {
392401
require(secnonce.size == Secp256k1.MUSIG2_SECRET_NONCE_SIZE)
393402
require(privkey.size == 32)
394403
require(keyaggCache.size == Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE)
395404
require(session.size == Secp256k1.MUSIG2_PUBLIC_SESSION_SIZE)
405+
require(musigNonceValidate(secnonce, pubkeyCreate(privkey)))
396406

397407
memScoped {
398408
val nSecnonce = alloc<secp256k1_musig_secnonce>()

tests/src/commonTest/kotlin/fr/acinq/secp256k1/Secp256k1Test.kt

+44-22
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ import kotlin.test.*
55

66
class Secp256k1Test {
77

8+
val random = Random.Default
9+
10+
fun randomBytes(length: Int): ByteArray {
11+
val buffer = ByteArray(length)
12+
random.nextBytes(buffer)
13+
return buffer
14+
}
15+
816
@Test
917
fun verifyValidPrivateKey() {
1018
val priv = Hex.decode("67E56582298859DDAE725F972992A07C6C4FB9F62A8FFF58CE3CA926A1063530".lowercase())
@@ -454,40 +462,55 @@ class Secp256k1Test {
454462

455463
@Test
456464
fun testMusig2SigningSession() {
457-
val privkeys = listOf(
458-
"0101010101010101010101010101010101010101010101010101010101010101",
459-
"0202020202020202020202020202020202020202020202020202020202020202",
460-
).map { Hex.decode(it) }.toTypedArray()
465+
val privkeys = listOf(randomBytes(32), randomBytes(32))
466+
val sessionId = randomBytes(32)
467+
val msg32 = randomBytes(32)
461468
val pubkeys = privkeys.map { Secp256k1.pubkeyCreate(it) }
462-
463-
val sessionId = Hex.decode("0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F0F")
464469
val nonces = pubkeys.map { Secp256k1.musigNonceGen(sessionId, null, it, null, null, null) }
470+
val testData = run {
471+
val builder = StringBuilder()
472+
builder.append("private keys\n")
473+
privkeys.forEach { builder.append(Hex.encode(it)).append("\n") }
474+
builder.append("sessionId ${Hex.encode(sessionId)}\n")
475+
builder.append("msg32 ${Hex.encode(msg32)}\n")
476+
builder.append("nonces\n")
477+
nonces.forEach { builder.append(Hex.encode(it)).append("\n") }
478+
builder.toString()
479+
}
465480
val secnonces = nonces.map { it.copyOfRange(0, 132) }
466481
val pubnonces = nonces.map { it.copyOfRange(132, 132 + 66) }
467482
val aggnonce = Secp256k1.musigNonceAgg(pubnonces.toTypedArray())
468483

469484
val keyaggCaches = (0 until 2).map { ByteArray(Secp256k1.MUSIG2_PUBLIC_KEYAGG_CACHE_SIZE) }
470485
val aggpubkey = Secp256k1.musigPubkeyAgg(pubkeys.toTypedArray(), keyaggCaches[0])
471-
assertContentEquals(aggpubkey, Secp256k1.musigPubkeyAgg(pubkeys.toTypedArray(), keyaggCaches[1]))
472-
assertContentEquals(keyaggCaches[0], keyaggCaches[1])
486+
assertContentEquals(aggpubkey, Secp256k1.musigPubkeyAgg(pubkeys.toTypedArray(), keyaggCaches[1]), testData)
487+
assertContentEquals(keyaggCaches[0], keyaggCaches[1], testData)
473488

474-
val msg32 = Hex.decode("0303030303030303030303030303030303030303030303030303030303030303")
475489
val sessions = (0 until 2).map { Secp256k1.musigNonceProcess(aggnonce, msg32, keyaggCaches[it]) }
476490
val psigs = (0 until 2).map {
477491
val psig = Secp256k1.musigPartialSign(secnonces[it], privkeys[it], keyaggCaches[it], sessions[it])
478-
assertEquals(1, Secp256k1.musigPartialSigVerify(psig, pubnonces[it], pubkeys[it], keyaggCaches[it], sessions[it]))
479-
assertEquals(0, Secp256k1.musigPartialSigVerify(Random.nextBytes(32), pubnonces[it], pubkeys[it], keyaggCaches[it], sessions[it]))
492+
assertEquals(1, Secp256k1.musigPartialSigVerify(psig, pubnonces[it], pubkeys[it], keyaggCaches[it], sessions[it]), testData)
493+
assertEquals(0, Secp256k1.musigPartialSigVerify(Random.nextBytes(32), pubnonces[it], pubkeys[it], keyaggCaches[it], sessions[it]), testData)
480494
psig
481495
}
482496

497+
// signing fails if the secret nonce does not match the private key's public key
498+
assertFails(testData) {
499+
Secp256k1.musigPartialSign(secnonces[1], privkeys[0], keyaggCaches[0], sessions[0])
500+
}
501+
502+
assertFails(testData) {
503+
Secp256k1.musigPartialSign(secnonces[0], privkeys[1], keyaggCaches[1], sessions[1])
504+
}
505+
483506
val sig = Secp256k1.musigPartialSigAgg(sessions[0], psigs.toTypedArray())
484-
assertContentEquals(sig, Secp256k1.musigPartialSigAgg(sessions[1], psigs.toTypedArray()))
485-
assertTrue(Secp256k1.verifySchnorr(sig, msg32, aggpubkey))
507+
assertContentEquals(sig, Secp256k1.musigPartialSigAgg(sessions[1], psigs.toTypedArray()), testData)
508+
assertTrue(Secp256k1.verifySchnorr(sig, msg32, aggpubkey), testData)
486509

487510
val invalidSig1 = Secp256k1.musigPartialSigAgg(sessions[0], arrayOf(psigs[0], psigs[0]))
488-
assertFalse(Secp256k1.verifySchnorr(invalidSig1, msg32, aggpubkey))
511+
assertFalse(Secp256k1.verifySchnorr(invalidSig1, msg32, aggpubkey), testData)
489512
val invalidSig2 = Secp256k1.musigPartialSigAgg(sessions[0], arrayOf(Random.nextBytes(32), Random.nextBytes(32)))
490-
assertFalse(Secp256k1.verifySchnorr(invalidSig2, msg32, aggpubkey))
513+
assertFalse(Secp256k1.verifySchnorr(invalidSig2, msg32, aggpubkey), testData)
491514
}
492515

493516
@Test
@@ -523,15 +546,14 @@ class Secp256k1Test {
523546
}
524547

525548
@Test
526-
fun fuzzEcdsaSignVerify() {
527-
val random = Random.Default
528-
529-
fun randomBytes(length: Int): ByteArray {
530-
val buffer = ByteArray(length)
531-
random.nextBytes(buffer)
532-
return buffer
549+
fun fuzzMusig2SigningSession() {
550+
repeat(1000) {
551+
testMusig2SigningSession()
533552
}
553+
}
534554

555+
@Test
556+
fun fuzzEcdsaSignVerify() {
535557
repeat(200) {
536558
val priv = randomBytes(32)
537559
assertTrue(Secp256k1.secKeyVerify(priv))

0 commit comments

Comments
 (0)